At Monterail we like to try new stuff. We try new languages and new (maybe not so new) frameworks.

Hanami (formerly Lotus) is a Ruby web framework created by Luca Guidi and community.

Probably the most problematic thing for people trying hanami after experience with rails is changing your mind away from the rails way. I can write many good things about hanami, but now I want to resolve one problem - authentication. The popular question is, What about users? In rails, we have devise which is a great gem but it is Rails only. In hanami, you can use other (non rails) user solutions. But sometimes you don't need all this devise stuff and omniauth is enough (like in our internal apps).

Step 1 - create new project

I guess you have some application already, but I'll create a new one.

hanami new hanami_oauth --test=rspec

For this tutorial we don't need any db (File System is the default adapter, so everything will be saved in file).

Step 2 - add user

The generator will create all necessary files. 

bundle exec hanami generate model user
create  lib/hanami_oauth/entities/user.rb
create  lib/hanami_oauth/repositories/user_repository.rb
create  spec/hanami_oauth/entities/user_spec.rb
create  spec/hanami_oauth/repositories/user_repository_spec.rb

Hanami uses hanami-model, but you can replace it with any other ORM. As we do not have any database we will not need migrations, but we must make some changes to our model.

Edit file: /lib/hanami_oauth.rb

Hanami::Model.configure do

...

  mapping do
    collection :users do
      entity     User
      repository UserRepository

      attribute :id,   Integer
      attribute :name, String
      attribute :github_id, String
      attribute :email, String
    end
  end
end

Next, edit file: lib/hanami_oath/entities/user.rb

class User
  include Hanami::Entity
  attributes :id, :name, :github_id, :email
end

Step 3 - add OmniAuth configuration

To connect our app with github we need to add 2 gems to our Gemfile

gem "omniauth-github"
gem "warden"

Add the github keys to .env. You can generate your keys here.

GITHUB_CLIENT_KEY="xxx"
GITHUB_CLIENT_SECRET="xxx"

Now let's prepare our application for OAuth.

edit: /apps/web/application.rb

Uncomment the following:

sessions :cookie, secret: ENV['WEB_SESSIONS_SECRET']

Also add this:

middleware.use Warden::Manager do |manager|
    manager.failure_app = Web::Controllers::Session::Failure.new
end

This:

middleware.use OmniAuth::Builder do
    provider :github, ENV["GITHUB_CLIENT_KEY"], ENV["GITHUB_CLIENT_SECRET"]
end

And this:

controller.prepare do
    include Web::Authentication
end

You can add before :authenticate! if you want to run it before all actions.

Step 4 - authentication controller

We need a global file and 3 actions (for new session, failure and destroy). At this moment views are redundant so we use generator with the --skip-view argument.

bundle exec hanami generate action web session#new --skip-view
bundle exec hanami generate action web session#failure --skip-view
bundle exec hanami generate action web session#destroy --skip-view

After that we should edit our router: /apps/web/config/routes.rb

get "/auth/failure", to: "session#failure"
get "/auth/signout", to: "session#destroy"
get "/auth/:provider/callback", to: "session#new"

If you want to see all routes you can type: bundle exec hanami routes in the terminal.

After that create this file: /app/web/controllers/authentication.rb

module Web
  module Authentication
    def self.included(action)
      action.class_eval do
        expose :current_user
      end
    end

    def current_user
      @current_user ||= warden.user
    end

    def warden
      request.env["warden"]
    end

    def authenticate_user!
      redirect_to "/auth/:provider/callback" unless current_user
    end
  end
end

Ok, so now we should create our actions.

Edit this file: /apps/web/controllers/session/failure.rb

module Web::Controllers::Session
  class Failure
    include Web::Action

    def call(_params)
      status 404, "Not found"
    end
  end
end

And this one: /apps/web/controllers/session/new.rb

module Web::Controllers::Session
  class New
    include Web::Action

    def auth_hash
      request.env["omniauth.auth"]
    end

    def call(params)
      user = UserRepository.auth!(auth_hash)
      warden.set_user user
      redirect_to "/"
    end

    def warden
      request.env["warden"]
    end
  end
end

And this one: /apps/web/controllers/session/destroy.rb

module Web::Controllers::Session
  class Destroy
    include Web::Action

    def call(params)
      warden.logout
      redirect_to "/"
    end
  end
end

Step 5 - add auth! to UserRepository

Again, edit this file: /lib/hanami_oauth/repositories/user_repository.rb

class UserRepository
  include Hanami::Repository

  def self.auth!(auth_hash)
    info = auth_hash[:info]
    github_id = info[:uid]
    attrs = {
      name:   info[:name],
      email:  info[:email],
    }

    if user = query { where(github_id: attrs[:github_id]) }.first
      user.update(attrs)
      update user
    else
      create(User.new(attrs.merge(github_id: github_id)))
    end
  end
end

Step 6 - add some views

We would probably like to check our authentication.

bundle exec hanami generate action web home#index

This will be enough.

We can set it as our main page in: apps/web/config/routes.rb get '/', to: 'home#index'

We created the first template in our app: /apps/web/templates/home/index.html.erb

<p>
<% if current_user %>
  Hello, <%= current_user.name %> | <%= link_to "Sign out", "/auth/signout" %>
<% else %>
  <%= link_to "Login with Github", "/auth/github" %>
<% end %>
</p>

Let's check our work

run hanami server in the terminal and open localhost:2300 you should see the following link Login with Github

Summary

If you have any problems, you can check the code on github. This application is very simple and does nothing except show the current user. But for this example, I think it's enough. You can use it with any other OAuth provider as well. I hope that after you overcome your first problem (authentication), delving into the hanami world will be pleasure.

Tobiasz Waszak avatar
Tobiasz Waszak