Skip to content

Engine mode

In Engine mode, your app mounts masks inside itself. masks keeps its own database, session, and secrets, apart from the app around it.

  1. Add the gem to your Gemfile:

    gem "masks-server"
  2. Run the install generator with one of these commands:

    Terminal window
    bin/rails generate masks:server:install --at /auth
    bin/rails generate masks:server:install --host auth.example.com
    • The first command mounts masks at /auth on your app’s own domain.
    • The second command serves masks at the root of a subdomain.

    Both write config/initializers/masks_server.rb:

    Rails.application.configure do
    config.masks.mode = :engine
    config.masks.database = :masks
    config.masks.tenant = "app"
    end

    --database and --tenant change the last two values. When the app has a config/puma.rb, the generator adds plugin :masks_ldap to it, so Puma also serves LDAP.

  3. Add a database for masks, as described below.

  4. Set config.masks.public_origin_template to the issuer, such as https://app.example.com/auth or https://auth.example.com. Outside development and test, masks does not boot without it.

  5. Run bin/rails db:prepare.

  6. Run bin/rails masks:tenants. It creates the tenant that config.masks.tenant names, and prints its setup token.

  7. Open /auth/login, or /login on the subdomain, and set up the first account with the setup token.

masks keeps its tables in a database of its own. Add it to config/database.yml in each environment:

masks:
<<: *default
database: myapp_masks_<%= Rails.env %>
username: masks
migrations_paths: <%= Masks::Server::Engine.root.join("db/migrate") %>
schema_dump: false

Its role must hold neither SUPERUSER nor BYPASSRLS. Row-level security, which limits each query to one tenant’s rows, keeps tenants apart, and those roles get around it. masks checks the role the first time it enters a tenant, and refuses to go on while the role holds either one.

When masks is mounted at a path, it shares your app’s origin. Your controllers read the signed-in actor from masks’ session, with no OAuth round trip:

class DashboardController < ApplicationController
include Masks::Server::Host
before_action :require_masks_actor!
def show
@actor = masks_actor
end
end

require_masks_actor! sends a signed-out visitor to masks’ sign-in page, and back again after they sign in. masks follows a return path only if the path is on your own app.

On a subdomain, masks has its own origin, and its cookies stay there. Sign your app in with Client mode, and set MASKS_ISSUER to that subdomain.

An app can sign in through Client mode against the masks it mounts, so that it holds access tokens and refresh tokens the same way it would with a separate masks server. Client mode fetches discovery, keys, and tokens over HTTP, and those calls go to the app’s own public address. Masks::Client.embed hands them to the engine inside the process, so they work when the app cannot reach its own public address:

Masks::Client.embed(->(env) { Rails.application.executor.wrap { Masks::Server::Engine.call(env) } }) do |uri|
uri.host == "auth.example.com"
end
  • The block names the URLs to keep in the process. Every other URL goes over the network.
  • A plain http URL is still refused unless its host is a loopback host or Masks::Client.plain_http is set.
  • Masks::Client.embed(nil) turns it off.

When masks is mounted at a path, set SCRIPT_NAME to the path and remove it from PATH_INFO before you call the engine.

masks uses
Tables Its own database, through config.masks.database.
Session Its own cookie, _masks_session. Signing in to masks never resets your app’s session.
Secrets config.masks.secret, or a secret derived from your app’s key under a label of its own. Your app’s cookie jar cannot read masks’ cookies.
Encrypted columns Its own key, derived the same way. From that key, one key for each tenant.
Jobs Its own job and mail classes carry a tenant. Your app’s jobs are left alone.
Setting
config.masks.mode :server or :engine. MASKS_MODE sets it too.
config.masks.database The database.yml entry masks uses in Engine mode. Required in Engine mode.
config.masks.tenant The tenant your app’s people belong to. MASKS_TENANT sets it too.
config.masks.secret The secret masks signs and encrypts with in Engine mode. Optional.
config.masks.public_origin_template The issuer, including the path it is mounted at. MASKS_PUBLIC_ORIGIN_TEMPLATE sets it too.

masks reads its other settings from the environment. See Environment variables.