Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
2
Help us understand the problem. What are the problem?

More than 3 years have passed since last update.

HanamiでGoogle OAuthを使った認証を実装しよう

HanamiでGoogle OAuthを使った認証を実装しましょう。大丈夫、超かんたんです。

完成品はこちらで見ることができます。

1. プロジェクトの作成

まずはプロジェクトを作成していきましょう。

$ gem install hanami
$ hanami new hanami_ominioauth_sample --test=rspec --template=slim
$ cd hanami_ominioauth_sample
$ bundle install

2. Userモデルの作成

次にUserモデルを作成します。

$ bundle exec hanami g model user
      create  lib/hanami_ominioauth_sample/entities/user.rb
      create  lib/hanami_ominioauth_sample/repositories/user_repository.rb
      create  db/migrations/20180102151137_create_users.rb
      create  spec/hanami_ominioauth_sample/entities/user_spec.rb
      create  spec/hanami_ominioauth_sample/repositories/user_repository_spec.rb
db/migrations/20180102151137_create_users.rb
Hanami::Model.migration do
  change do
    create_table :users do
      primary_key :id

      column :provider  , String , null: false
      column :uid       , String , null: false
      column :name      , String , null: false
      column :email     , String , null: false
      column :image_path, String , null: true

      column :created_at, DateTime, null: false
      column :updated_at, DateTime, null: false

      index [:provider, :uid], unique: true
      index [:email]         , unique: true
    end
  end
end
$ bundle exec hanami db prepare
[hanami] [INFO] (0.000842s) PRAGMA foreign_keys = 1
[hanami] [INFO] (0.000041s) PRAGMA case_sensitive_like = 1
[hanami] [INFO] (0.000900s) SELECT sqlite_version()
[hanami] [ERROR] SQLite3::SQLException: no such table: schema_migrations: SELECT NULL AS 'nil' FROM `schema_migrations` LIMIT 1
[hanami] [INFO] (0.002738s) CREATE TABLE `schema_migrations` (`filename` varchar(255) NOT NULL PRIMARY KEY)
[hanami] [ERROR] SQLite3::SQLException: no such table: schema_info: SELECT NULL AS 'nil' FROM `schema_info` LIMIT 1
[hanami] [INFO] (0.000211s) SELECT `filename` FROM `schema_migrations` ORDER BY `filename`
[hanami] [INFO] Begin applying migration 20180102151137_create_users.rb, direction: up
[hanami] [INFO] (0.001833s) CREATE TABLE `users` (`id` integer NOT NULL PRIMARY KEY AUTOINCREMENT, `provider` varchar(255) NOT NULL, `uid` varchar(255) NOT NULL, `name` varchar(255) NOT NULL, `email` varchar(255) NOT NULL, `image_path` varchar(255) NULL, `created_at` timestamp NOT NULL, `updated_at` timestamp NOT NULL)
[hanami] [INFO] (0.001225s) CREATE UNIQUE INDEX `users_provider_uid_index` ON `users` (`provider`, `uid`)
[hanami] [INFO] (0.001379s) CREATE UNIQUE INDEX `users_email_index` ON `users` (`email`)
[hanami] [INFO] (0.001321s) INSERT INTO `schema_migrations` (`filename`) VALUES ('20180102151137_create_users.rb')
[hanami] [INFO] Finished applying migration 20180102151137_create_users.rb, direction: up, took 0.007446 seconds

ひとまずこれでUserモデルができました。

3. Homeページの作成

ログインボタンを置くHomeページを作成します。

$ bundle exec hanami g action web page#home
      create  apps/web/controllers/page/home.rb
      create  apps/web/views/page/home.rb
      create  apps/web/templates/page/home.html.slim
      create  spec/web/controllers/page/home_spec.rb
      create  spec/web/views/page/home_spec.rb
      insert  apps/web/config/routes.rb
apps/web/config/routes.rb
root                             to: 'page#home'
get  '/home'                   , to: 'page#home'
apps/web/templates/page/home.html.slim
h1 Home
$ bundle exec hanami s

localhost:2300 を開いて「Home」と表示されていればOKです。

4. 認証の実装

A. クライアントIDの取得

まず、次のURLを開いてください。
https://console.developers.google.com

上のメニューからプロジェクト名を「Hanami Ominioauth Sample」にして作成します。作成には30秒ほどかかります。作成できたら、「Hanami Ominioauth Sample」を開きましよう。

ominiauth01.png

左側に「ライブラリ」というメニューがあるのでクリックします。

ominiauth02.png

ここで以下の2つを有効にしてください。

  • Contacts API
  • Google+ API

次に認証情報を作成しましす。「認証情報」をクリックしてください。

ominiauth03.png

タブから「OAuth 同意画面」を選択します。

  • メールアドレス: 初期値
  • ユーザーに表示するサービス名: 「Hanami Ominioauth Sample」
  • ホームページの URL: ブランク
  • サービスロゴの URL: ブランク
  • プライバシー ポリシーの URL: ブランク
  • 利用規約の URL: ブランク

「認証情報を作成」 -> 「OAuth クライアントID」を選択します。

以下の内容を入力して「作成」を2回クリックします。

「クライアントID」と「クライアントシークレット」が表示されるのでこれを控えておきます。

B. (option) direnv の導入

先程入手した「クライアントID」と「クライアントシークレット」を管理するのにdirenvを使うと便利です。

macの場合はHomebrewでインストールできます。

$ brew install direnv

そして.ernvrcを作成します。

.envrv
export GOOGLE_CLIENT_ID=[クライアントID]
export GOOGLE_CLIENT_SECRET=[クライアントシークレット]

あとはこれを適用してあげればOKです。

$ direnv allow

gitを使う場合は.gitignoreに入れておきましょう。

.gitignroe
+ .envrc

C.ominiauthを使う準備

いよいよ認証部分を実装していきます。omniauth-google-oauth2というgemを使っていきます。

Gemfile
+ gem 'omniauth-google-oauth2'
$ bundle install

application.rb を書き換えてセッションを有効にし、さらにomniauthを使うように設定します。

apps/web/application.rb
      # Enable sessions
      # Argument: Symbol the Rack session adapter
      #           A Hash with options
      #
      # See: http://www.rubydoc.info/gems/rack/Rack/Session/Cookie
      #
-     # sessions :cookie, secret: ENV['WEB_SESSIONS_SECRET']
+     sessions :cookie, secret: ENV['WEB_SESSIONS_SECRET']

...

      # Configure Rack middleware for this application
      #
-     # middleware.use Rack::Protection
+     middleware.use OmniAuth::Builder do
+       provider :google_oauth2, ENV['GOOGLE_CLIENT_ID'], ENV['GOOGLE_CLIENT_SECRET']
+     end

D. sessionコントローラーの作成

sessionコントローラーを作成します。

$ bundle exec hanami g action web session#create
      create  apps/web/controllers/session/create.rb
      create  apps/web/views/session/create.rb
      create  apps/web/templates/session/create.html.slim
      create  spec/web/controllers/session/create_spec.rb
      create  spec/web/views/session/create_spec.rb
      insert  apps/web/config/routes.rb

自動生成されたコードのうちroutes.rbをとりあえず書き換えます。

apps/web/config/routes.rb
- post '/session', to: 'session#create'
+ post '/auth/:provider/callback', to: 'session#create'
+ get  '/auth/:provider/callback', to: 'session#create'

E. インタラクタの作成とコントローラの実装

認証部分の実装をコントローラに書いていくことも可能ですが、コントローラにはあまりロジックを書きたくないので、今回はインタラクタを使います。インタラクタとはいわゆるサービスです。くわしくはHanamiのサービス(インタラクタ)層についてを読んでいただけると幸いです。

$ mkdir lib/hanami_ominioauth_sample/interactors
lib/hanami_ominioauth_sample/interactors/find_or_create_user_for_auth.rb
require 'hanami/interactor'

class FindOrCreateUserForAuth
  include Hanami::Interactor

  expose :user

  def initialize repository: UserRepository.new
    @repository = repository
  end

  def call auth
    @user = @repository.find_by_provider_and_uid(auth.provider, auth.uid) || @repository.register(auth)
  end
end

#find_by_provider_and_uidメソッドと#registerメソッドを使うコードを書いたのでこれをUserRepositoryに実装します。

lib/hanami_ominioauth_sample/repositories/user_repository.rb
class UserRepository < Hanami::Repository
  def find_by_provider_and_uid provider, uid
    users.where(provider: provider, uid: uid).one
  end

  def register auth
    create(
      provider:   auth.provider,
      uid:        auth.uid,
      name:       auth.info.name,
      email:      auth.info.email,
      image_path: auth.info.image,
    )
  end
end

あとはコントローラ側でリポジトリを使うように実装していきます。

apps/web/controllers/session/create.rb
module Web::Controllers::Session
  class Create
    include Web::Action

    def call _params
      auth = request.env['omniauth.auth']
      result = FindOrCreateUserForAuth.new(repository: UserRepository.new).call(auth)
      if result.success?
        flash[:success] = "Login seccessed"
      else
        flash[:error] = "Login failed"
      end
      redirect_to routes.root_path
    end
  end
end

最後にtemplateを更新します。

apps/web/templates/page/home.html.slim
  h1 Home

+ p = flash[:success]
+ p = flash[:error]

+ a(href="/auth/google_oauth2") Login

Hanamiサーバーを起動して localhost:2300 を開きLogin ボタンをクリックしてみましょう。Googleの認証画面が開き、アカウント選択後、元の画面に戻って「Login seccessed」と表示されていればOKです。

念のためデータができているかも確認してみましょう。

$ bundle exec hanami c
irb(main):001:0> UserRepository.new.all
[hanami_ominioauth_sample] [INFO] [2018-01-03 01:20:49 +0900] (0.008787s) SELECT `id`, `provider`, `uid`, `name`, `email`, `image_path`, `created_at`, `updated_at` FROM `users` ORDER BY `users`.`id`
=> [#<User:0x00007fcbce94f560 @attributes={:id=>1, :provider=>"google_oauth2", :uid=>"dummy", :name=>"Isuke FUJIYAMA", :email=>"isuke_dummy@gmail.com", :image_path=>"https://dummy/photo.jpg", :created_at=>2018-01-02 16:15:45 UTC, :updated_at=>2018-01-02 16:15:45 UTC}>]

5. セッション管理の実装

さて、ユーザーに毎回ログインさせるのは忍びないので、セッションでログイン情報を管理しましょう。

apps/web/controllers/session/create.rb
    def call _params
      auth = request.env['omniauth.auth']
      result = FindOrCreateUserForAuth.new(repository: UserRepository.new).call(auth)
      if result.success?
+       session[:provider] = result.user.provider
+       session[:uid]      = result.user.uid

        flash[:success] = "Login seccessed"
      else
        flash[:error] = "Login failed"
      end
      redirect_to routes.root_path
    end

セッションを扱う各コントローラに共通の部分を実装していきます。apps/web/controllers/authentication.rbというファイルを新規で作成して、以下のようにします。。

apps/web/controllers/authentication.rb
module Web
  module Authentication
    def self.included action
      action.class_eval do
        before :current_user
        before :authenticate!
        expose :current_user
      end
    end

  private

    def authenticate!
      halt 401 unless authenticated?
    end

    def authenticated?
      !current_user.nil?
    end

    def current_user
      @current_user ||= UserRepository.new.find_by_provider_and_uid(session[:provider], session[:uid])
    end
  end
end

さて、これをすべてのコントローラでincludeする必要がありますが、毎回書くのはだるいです。application.rbに記述することで対応しましょう。

apps/web/application.rb
      # Configure the code that will yield each time Web::Action is included
      # This is useful for sharing common functionality
      #
      # See: http://www.rubydoc.info/gems/hanami-controller#Configuration
      controller.prepare do
-       # include MyAuthentication # included in all the actions
-       # before :authenticate!    # run an authentication before callback
+       include Web::Authentication
      end

これで各コントローラでauthentication.rbで定義した内容が使えるようになったので、画面に反映させてみましょう。

apps/web/templates/page/home.html.slim
  h1 Home

  p = flash[:success]
  p = flash[:error]

+ - if current_user.nil?
    a(href="/auth/google_oauth2") Login
+ - else
+   image(src=current_user.image_path)

これでログイン済みの状態のときに、ログインユーザのアイコンが表示されていればOKです。

さて、現状だとすべてのページがログイン必須になっているのでログインボタンがあるHomeページすらログインしないと開けません(!)。Homeページは認証をスキップするようにしましょう。と、いってもHanamiにはRailsと違ってskip_before_actionのようなものはないのでoverrideするだけです。

apps/web/controllers/page/home.rb
  module Web::Controllers::Page
    class Home
      include Web::Action

      def call(params)
      end

+   private

+     def authenticate!
+       # no-op
+     end
    end
  end
apps/web/controllers/session/create.rb
module Web::Controllers::Session
    class Create
      include Web::Action

      def call _params
        ...
      end

+   private

+     def authenticate!
+       # no-op
+     end
+   end
  end

以上で終わりです。お疲れ様でした。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
2
Help us understand the problem. What are the problem?