Authentication
How Dashboard Authentication Works
On non-Rails frameworks, the Sentiero dashboard is open to anyone by default. Set basic_auth, auth_callback, or route-level auth before deploying. The Rails installer enables basic_auth by default (see below).
auth_callback
The auth_callback configuration option receives the Rack env hash and should return a truthy value to allow access:
Sentiero.configure do |config|
config.auth_callback = ->(env) { env["rack.session"]&.dig("admin") }
end
The check lives in Sentiero::Web::BaseApp (shared by DashboardApp, AnalyticsApp, and all dashboard routes): a nil callback means open access, a truthy return allows the request, and a falsy return or a raised exception yields 403. It fails closed.
Key behaviors:
nilcallback = open access. Every request is allowed. This is the default.- Truthy return = allowed. The double-bang (
!!) coerces any truthy value. - Falsy return = 403 Forbidden. Plain text response, no redirect.
- Exceptions = 403. If your callback raises, it’s caught, logged to stderr, and treated as denied. Fail closed.
Every dashboard route checks authorized?(env) before proceeding. The only exception is /assets/* (static CSS/JS/images), which is intentionally public and has path traversal protection.
basic_auth: Built-in HTTP Basic Auth
Setting basic_auth protects the dashboard and analytics UI directly at Sentiero’s auth gate, no middleware wiring required, on any framework (Rails, Roda, Sinatra, plain Rack):
Sentiero.configure do |config|
config.basic_auth = { user: "admin", password: ENV["SENTIERO_DASHBOARD_PASSWORD"] }
end
Key behaviors:
- Events and assets stay public. Only the dashboard and analytics routes are protected.
- Blank password raises
Sentiero::Error. If the configured password is blank (e.g., the env var is unset), Sentiero raises an error at request time rather than silently allowing access. Fail closed. - Returns
401withWWW-Authenticate. Browsers display their native login dialog. Wrong or missing credentials = 401; this is distinct fromauth_callback’s 403. basic_authandauth_callbackare independent. Whenbasic_authis set it is the authority;auth_callbackis only used whenbasic_authisnil.- Rails enables this by default. The
rails generate sentiero:installgenerator produces an initializer withconfig.basic_authactive and prints a generated password forSENTIERO_DASHBOARD_PASSWORD. To disable, comment out the block.
Credentials are compared with Rack::Utils.secure_compare (constant-time, timing-safe). Assumes TLS terminated upstream. Do not deploy over plain HTTP.
A standalone Sentiero::Web::BasicAuth Rack middleware also exists for fully-standalone dashboard deployments where you want to mount the dashboard separately without the full Sentiero configuration object.
Open by default (non-Rails). On Rails,
basic_authis enabled by the generator. On other frameworks, if neitherbasic_authnorauth_callbackis configured, the dashboard is accessible to anyone who can reach the URL. Always set at least one form of protection before deploying to production.
The 403 Limitation
This is the most important thing to understand: auth_callback always returns 403, never 401 or 302.
This means:
| Auth type | Works with auth_callback? |
Why |
|---|---|---|
| Session-based (Devise, Warden, Rodauth, custom cookies) | Yes | User already has a session. 403 = “you’re logged in but not allowed.” |
| HTTP Basic Auth | No | Browsers need a 401 with WWW-Authenticate header to show the login dialog. 403 skips it. |
| OAuth / SSO redirects | No | Needs a 302 redirect to the auth provider. 403 is a dead end. |
| Token auth (API keys, Bearer tokens) | Partially | Works if the token is already in the request (e.g., header or cookie). Won’t prompt for one. |
When auth_callback won’t work, use route-level auth. Handle authentication in your framework’s routing layer before the request reaches DashboardApp.
Integration Examples
Rails with Devise
Option A: auth_callback (if users are already signed in via Devise session)
# config/initializers/sentiero.rb
Sentiero.configure do |config|
config.auth_callback = ->(env) {
warden = env["warden"]
user = warden&.user
user&.admin?
}
end
# config/routes.rb
mount Sentiero::Web::EventsApp.new => "/sentiero/events"
mount Sentiero::Web::DashboardApp.new => "/sentiero"
Option B: Route constraint (uses Devise’s authenticate to redirect to login)
# config/routes.rb
mount Sentiero::Web::EventsApp.new => "/sentiero/events"
# Serve recorder.js publicly; only the dashboard UI is gated.
mount Sentiero::Web::AssetsApp.new => "/sentiero/assets"
authenticate :user, ->(user) { user.admin? } do
mount Sentiero::Web::DashboardApp.new => "/sentiero"
end
This is better if unauthenticated users should be redirected to the sign-in page rather than seeing a 403.
Warning: the
AssetsAppmount is not optional here. A route constraint gates everything under the mount path, including/sentiero/assets/*(the recorder JS, whichDashboardAppwould otherwise serve before its own auth check). Without the publicAssetsAppmount, anonymous visitors get a 401/redirect onrecorder.jsand recording silently breaks for everyone except logged-in admins. If you don’t need a login redirect, preferauth_callback(Option A), where assets short-circuit before the callback runs and no separate mount is required.
Rails with Custom Session Auth
Sentiero.configure do |config|
config.auth_callback = ->(env) {
session = env["rack.session"]
session && session["user_role"] == "admin"
}
end
Roda with HTTP Basic Auth
Set config.basic_auth in your Sentiero configuration. This is the pattern from the demo app (demo/app.rb):
Sentiero.configure do |config|
config.basic_auth = { user: ENV["DASHBOARD_USER"], password: ENV["DASHBOARD_PASSWORD"] }
end
No additional route-level wiring is needed. The dashboard routes are protected automatically, the events endpoint stays public, and browsers see a 401 WWW-Authenticate challenge.
For session- or role-based auth (where you need a redirect rather than a Basic challenge), use auth_callback or route-level auth instead.
Roda with Rodauth
Option A: auth_callback (session-based, user already logged in)
Sentiero.configure do |config|
config.auth_callback = ->(env) {
scope = env["roda.rodauth"] # depends on your Rodauth setup
scope&.logged_in? && scope.account[:role] == "admin"
}
end
Option B: Route-level (redirects to login page if not authenticated)
class MyApp < Roda
plugin :sentiero
plugin :rodauth do
# ... rodauth config
end
route do |r|
r.on "sentiero" do
r.on("events") { r.sentiero_events }
rodauth.require_account
unless rodauth.account[:role] == "admin"
r.halt [403, {"content-type" => "text/plain"}, ["Forbidden"]]
end
r.sentiero_dashboard
end
end
end
Sinatra
Option A: basic_auth (HTTP Basic, browser prompts for credentials)
Sentiero.configure do |config|
config.basic_auth = { user: ENV["DASHBOARD_USER"], password: ENV["DASHBOARD_PASSWORD"] }
end
No middleware wiring needed. Events and assets stay public; dashboard routes are protected.
Option B: auth_callback (session-based)
Sentiero.configure do |config|
config.auth_callback = ->(env) {
env["rack.session"]&.dig("user", "admin")
}
end
Plain Rack
Use config.basic_auth; no middleware wiring needed:
# config.ru / boot
Sentiero.configure do |config|
config.basic_auth = { user: ENV["DASHBOARD_USER"], password: ENV["DASHBOARD_PASSWORD"] }
end
map "/sentiero/events" do
run Sentiero::Web::EventsApp.new
end
map "/sentiero" do
run Sentiero::Web::DashboardApp.new
end
For session-based checks (e.g., redirect to login), write standard Rack middleware that checks the session and either calls @app.call(env) or returns a 302, then wrap the dashboard mount with it:
map "/sentiero" do
use DashboardAuth # your middleware: allow if logged in, else redirect to /login
run Sentiero::Web::DashboardApp.new
end
The Events Endpoint Is Public
EventsApp (POST /sentiero/events) has no authentication by design. It receives rrweb event data from the user’s browser.
Why not add auth? The recorder JavaScript runs client-side. Any credentials embedded in client JS are visible to anyone who views source. A dedicated attacker can always POST fake events. Authentication would add complexity without meaningful security.
Instead, protect it with:
- CORS origins:
config.cors_origins = ["https://yoursite.com"]. Browsers enforce this; won’t stop curl but blocks cross-origin JS. - Rate limiting: Use Rack::Attack or nginx
limit_reqto throttle event submissions per IP. - Payload size limits: Sentiero enforces a 512KB per-request limit. The recorder splits large payloads automatically.
- Resource limits:
max_events_per_request,max_sessions, andmax_events_per_sessionprevent unbounded storage growth.
CSRF Protection
State-changing dashboard operations (delete, bulk delete) are protected by a double-submit cookie pattern:
- The index page generates a random token and sets it as both a cookie (
sentiero_csrf) and a hidden form field. - On submit, the server compares the cookie value to the form value using
Rack::Utils.secure_compare(timing-safe). - Mismatch or missing values result in
403 Invalid CSRF token.
Cookie attributes:
HttpOnly: not accessible to JavaScriptSameSite=Strict: only sent on same-site requestsSecure: set automatically when served over HTTPSPath: scoped to the dashboard mount point
Read-only operations (GET requests) don’t require CSRF tokens.
Production Checklist
See the Production Checklist in the README for the full list. Auth-specific items:
- Set
basic_authorauth_callback. On Rails the generator enablesbasic_authby default. Do not deploy with an open dashboard. - Set
cors_originsto your frontend’s origin(s). - Add rate limiting on the events endpoint.
- Serve over HTTPS so the
Secureflag is set on the CSRF cookie.