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.