Sign a Rails app in
Install
Section titled “Install”# Gemfilegem "masks"bin/rails generate masks:installThat mounts the engine at /auth, writes config/initializers/masks.rb, and gitignores the file
credentials land in. --mount puts it somewhere else; --resource fills in the identifier when this
app also accepts tokens of its own.
One gem carries both halves, and the engine loads only when Rails does — so the same gem "masks" in
a Rack or API-only app pulls in jwt and nothing else. See verifying a token.
If you would rather do it by hand, the whole of the generator’s routing is one line:
mount Masks::Rails::Engine, at: "/auth"Configure
Section titled “Configure”Masks::Rails.configure do |config| config.issuer = ENV.fetch("MASKS_ISSUER") config.credentials = -> { { client_id: ENV["MASKS_CLIENT_ID"], client_secret: ENV["MASKS_CLIENT_SECRET"] } } config.scope = %w[openid profile email offline_access] config.after_sign_in = "/"endIf you have no credentials to name — which is the usual case, since nobody should be copying a
secret into an environment file — leave credentials reading wherever you store them and give the
engine a store. It will run the handshake at /auth/handshake and put
them there.
Every value also accepts a callable taking the request, which is how a subdomain-per-tenant application points each host at its own issuer:
config.issuer = ->(request) { "https://#{request.host.split(".").first}.auth.example" }redirect_uri defaults to your /auth/callback on the requesting host, so it usually needs no
configuration at all — but it must match a URI the client was approved for.
Require sign-in
Section titled “Require sign-in”class ApplicationController < ActionController::Base before_action :authenticate_masks!endauthenticate_masks! returns true if there is a live session, silently refreshes if the access token
has expired and a refresh token is available, and otherwise remembers where you were headed and
redirects into the flow. An app holding no credentials yet is sent to the handshake instead, and a
JSON caller is answered handshake_required — never a login it cannot complete.
Read who signed in
Section titled “Read who signed in”masks_signed_in? |
is there an unexpired token |
masks_identity |
verified id token claims — sub, preferred_username, email |
masks_tenant |
the tenant claim: uuid, subdomain, name |
masks_tokens |
the raw Tokens, including access_token for calling APIs |
The first three are available in views.
<% if masks_signed_in? %> Signed in as <%= masks_identity["preferred_username"] %> at <%= masks_tenant["name"] %><% end %>Map a tenant
Section titled “Map a tenant”masks owns tenants; your application holds a projection keyed by the uuid.
def current_tenant claim = masks_tenant or return nil
Tenant.find_or_create_by!(uuid: claim["uuid"]) do |tenant| tenant.subdomain = claim["subdomain"] tenant.name = claim["name"] endendKey on uuid, not subdomain — a subdomain is routing and can be renamed.
What the callback checks
Section titled “What the callback checks”state is compared in constant time against the value stashed when the flow began, and the id
token’s nonce is compared against its own stashed value. Both fail closed. The PKCE verifier is
deleted from the session as it is used, so a replayed callback has nothing to exchange with.
