ruby 89 lines · 3 tabs

Protecting Namespaced Admin Routes in Sinatra with an HTTP Basic Auth before Filter

Shared by codesnips Aug 2026
3 tabs
require "sinatra/base"
require "rack/utils"

class AdminApp < Sinatra::Base
  before do
    protected!
  end

  get "/" do
    "Admin dashboard"
  end

  get "/metrics" do
    content_type :json
    { queued: 12, failed: 0 }.to_json
  end

  post "/flush" do
    status 202
    "flush scheduled"
  end

  helpers do
    def protected!
      return if authorized?
      request_auth!
    end

    def request_auth!
      headers["WWW-Authenticate"] = %(Basic realm="Admin Area")
      halt 401, "Not authorized\n"
    end

    def authorized?
      @auth ||= Rack::Auth::Basic::Request.new(request.env)
      return false unless @auth.provided? && @auth.basic? && @auth.credentials

      user, pass = @auth.credentials
      Rack::Utils.secure_compare(user, ENV.fetch("ADMIN_USER")) &&
        Rack::Utils.secure_compare(pass, ENV.fetch("ADMIN_PASSWORD"))
    end
  end
end
3 files · ruby Explain with highlit

This snippet shows how a modular Sinatra application isolates its admin surface behind a single HTTP Basic auth gate rather than sprinkling authentication checks into every handler. The core idea is that a before filter combined with a shared route prefix lets one block guard an entire namespace, so new admin endpoints inherit protection automatically.

In AdminApp, all routes live under /admin because the whole Sinatra::Base subclass is mounted at that path in config.ru. The before filter runs ahead of every matching request and delegates to protected!, which either passes through or halts. protected! checks a memoized Rack::Auth::Basic::Request wrapper around env; when credentials are missing or wrong it calls halt 401, immediately short-circuiting the request so no route body ever executes. The WWW-Authenticate header set in request_auth! is what makes a browser show the native login dialog, and returning status 401 is what tells the client to retry with credentials.

The credential comparison in authorized? is deliberately written with Rack::Utils.secure_compare instead of ==. A naive string comparison returns early on the first differing byte, leaking timing information an attacker can exploit to guess a secret character by character; the constant-time compare closes that side channel. Expected values are pulled from ENV so secrets stay out of source control, with fetch raising loudly if they are unset rather than silently allowing an empty password.

In config.ru, the public MainApp and the guarded AdminApp are composed with Rack::URLMap via map, giving a clean separation where only the /admin mount carries the auth filter. This keeps the public app completely free of auth concerns.

The pattern's trade-off is that Basic auth sends credentials on every request and offers no logout or session semantics, so it suits internal dashboards and CI-only tooling behind TLS, not consumer login. The Rack::Test example in admin_auth_spec.rb verifies both the 401 challenge and a successful authenticated request using the authorize helper, documenting the contract the filter enforces.


Related snips

Share this code

Here's the card — post it anywhere.

Protecting Namespaced Admin Routes in Sinatra with an HTTP Basic Auth before Filter — share card
Link copied