Authenticated routes with Hanami 2

As I was building out the admin area of this blog, I wanted to make sure only authenticated people could hop in an edit things.

The first iteration of this, was based on HTTP BasicAuth. To get this to work, I built a small module that uses Rack::Auth::Basic β€” https://github.com/rack/rack/blob/main/lib/rack/auth/basic.rb.

It looked like:

class AuthenticatedAdminAction  
  def self.call(action:)  

    action = ->(env) { Admin::Container["actions.#{action}"].(env) }  

    basic_auth_username = Hanami.app.settings.basic_auth_username
    basic_auth_password = Hanami.app.settings.basic_auth_password  

    if basic_auth_username && basic_auth_password
      Rack::Auth::Basic.new(action) do |username, password|  
        username == basic_auth_username &&  
          password == basic_auth_password  
      end  
    else      
        Rack::Auth::Basic.new(action_proc) { |_username, _password| true }  
    end  
  end

Then, on each path that I wanted to authenticate, I would adjust the to: argument call this method:

Auth = AuthenticatedAdminAction

get "/", to: Auth.call(action: "index")

Which looks nice, and works well! However, something changed in the way Safari on iOS and macOS handles auto-filling usernames and passwords for HTTP Basic Auth that meant it would log me out every few minutes and I would need to re-open 1Password to log back in!

I thought this may have been a beta-season thing (after seeing a few other cases of it online), so I stuck it out for a while. But after many months of being on a stable version of Sonoma, the problem persisted!

So, onwards with a more stable and scalable form of authentication. It didn't need to be fancy, it just needed to let me create a session with as little hassle as possible. For this reason, I didn't opt for something like OmniAuth or even Warden and instead built a simple token-based auth system that emails me a login link.

The above code changes slightly, to now call some custom middleware with the user in the session, but is now a zillion times more reliable as Safari doesn't seem to forget who I am every 2 seconds πŸ™ƒ.

The middleware now looks something like:

module Middleware  
  class Authenticate  
    def initialize(app, auth_proc)  
      @app = app  
      @auth_proc = auth_proc  
    end  

    def call(env)  
      session = env["rack.session"]  
      return [403, {"Content-Type" => "text/html"}, ["Unauthorized | <a href=\"/admin/login\">Login</>"]] unless @auth_proc.call(session[:user_id])  

      @app.call(env)  
    end  
  endend

Next steps

The next thing I want to look at is wrapping a block of routes in an authentication block, e.g:

AuthenticatedThing.call do
  get "/pages", to: "pages.index"
  get "/pages/new", to: "pages.new"
  get "/pages/:slug/edit", to: "pages.edit"
end

mainly to make it easier to see which routes are authenticated and avoid accidentally missing some routes.

Published

by Daniel Nitsikopoulos

in posts

Tagged

Β© 2010 - 2024 Daniel Nitsikopoulos. All rights reserved.

πŸ•ΈπŸ’  →