🚀

Rails 6 + mongoid + DeviseでのTwitterアカウントによるSSO

2021/11/14に公開

この記事では書かない前提

https://zenn.dev/yuiseki/articles/890ba465b4620b

参考文献

https://techblog.gmo-ap.jp/2019/02/08/railsでomniauth-twitterを利用してtwitter認証を実装する/

https://remoter.hatenablog.com/entry/2020/07/07/複数モデルでdeviseを利用するときに特定のモデルだ

https://zenn.dev/kitabatake/articles/start-to-like-the-devise

その他、多数の記事を参考にさせていただきました。

概要

使用ライブラリ

  • devise
  • omniauth
  • omniauth-twitter

仕様

  • Twitterアカウントでしかログインできない前提にする
  • DeviseはEmailとPasswordログインを前提としているのでそれらのフィールドは消す

RailsにDeviseを導入する

  • Gemfileに以下を追記する
gem 'devise'
gem 'omniauth', '1.9.1'
gem 'omniauth-twitter'

omniauthを1.9.1にしないとハマるという情報があったのでバージョン指定しているけど最新バージョンで直っているかも知れない

  • bundle install する

DeviseでUserモデルを初期化する

  • 以下のコマンドを実行
rails g devise:install
rails g devise User

DeviseはActiveRecordじゃなくてmongoidが入っている場合はちゃんとそれを理解したコードを生成してくれる。素晴らしい。あとで全部消すけど。

重要

  • rails g devise:viewしてはいけない
    • Email, PasswordでサインアップやサインインするためのViewが生成されてしまうが不要なため

omniauth-twitterの導入

Users::OmniauthCallbacksControllerをつくる

  • 以下のコマンドを実行
rails g controller users/omniauth_callbacks
rails routes
  • 以下を確認
    • app/controllers/users/omniauth_callbacks_controller.rb が生成されたこと
    • user_twitter_omniauth_callback pathが上記のコントローラーになっていること

Users::OmniauthCallbacksControllerを実装する

  • app/controllers/users/omniauth_callbacks_controller.rb をまるっと以下のように書き換える
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
  def twitter
    callback_from :twitter
  end

  private

  def callback_from(provider)
    provider = provider.to_s

    @user = User.twitter_oauth(request.env['omniauth.auth'])

    if @user.persisted?
      flash[:notice] = I18n.t('devise.omniauth_callbacks.success', kind: provider.capitalize)
      sign_in_and_redirect @user, event: :authentication
    else
      session["devise.#{provider}_data"] = request.env['omniauth.auth']
      redirect_to root_path
    end
  end
end

Deviseでomniauth-twitterを使うように設定

config/initializers/devise.rb に以下を追記

  config.omniauth :twitter, ENV['TWITTER_TOKEN'], ENV['TWITTER_SECRET'], oauth_callback: "#{ENV['ORIGIN']}#{user_twitter_omniauth_callback_path}"
  OmniAuth.config.logger = Rails.logger if Rails.env.development?

config/routes.rb を以下のように変更

devise_for :users, controllers: { omniauth_callbacks: 'users/omniauth_callbacks' }

.env ファイルを設置する

ORIGIN=http://localhost:3000/
TWITTER_TOKEN=https://dev.twitter.com/で確認できるやつ
TWITTER_SECRET=https://dev.twitter.com/で確認できるやつ

Userモデルにフィールドとメソッドを追加

ポイント

  • rails g devise User で生成された app/models/user.rb の内容をまるっと書き換える
    • TwitterでのSSOにしたいので、Email, Passwordは不要にしたい
    • しかしDeviseの :database_authenticatable はEmail, Passwordが必須
      • authentication_keys: [:uid] を指定し self.find_for_database_authentication を実装する
      • field :twitter_token, as: :encrypted_password としてDevise(が内部で使っているWarden)が encrypted_password attributeにアクセスできるようにしてやる
  • mongoidなのでマイグレーションは不要
class User
  include Mongoid::Document
  include Mongoid::Timestamps

  devise :database_authenticatable, :rememberable, :omniauthable, authentication_keys: [:uid]

  ## Rememberable
  field :remember_created_at, type: Time

  # OmniAuthable
  field :provider, type: String
  field :uid, type: String

  # Twitter
  field :twitter_id, type: String, default: ""
  field :twitter_name, type: String, default: ""
  field :twitter_icon, type: String, default: ""
  field :twitter_token, as: :encrypted_password, type: String, default: ""
  field :twitter_secret, type: String, default: ""

  def self.twitter_oauth(auth)
    user = User.where(uid: auth.uid, provider: auth.provider).first

    if user
      user.update(
        twitter_id: auth.info.nickname,
        twitter_name: auth.info.name,
        twitter_icon: auth.info.image,
        twitter_token: auth.credentials.token,
        twitter_secret: auth.credentials.secret,
      )
    else
      user = User.create(
        provider: auth.provider,
        uid: auth.uid,
        twitter_id: auth.info.nickname,
        twitter_name: auth.info.name,
        twitter_icon: auth.info.image,
        twitter_token: auth.credentials.token,
        twitter_secret: auth.credentials.secret,
      )
    end
    user
  end

  def self.find_for_database_authentication(warden_conditions)
    uid = warden_conditions[:uid].to_s.downcase.strip
    find_by(uid: uid)
  end
end

ログイン画面を作る

以下のコマンドを実行

rails g controller Root index

app/views/root/index.html.erb を以下のように書き換える

<% if user_signed_in? %>
  <div>
    <%= link_to 'Sign out', destroy_user_session_path, method: :delete %>
  </div>
  <h2>
    Twitter account:
      @<%= current_user.twitter_id %>
      (<%= current_user.twitter_name %>)
    <%= image_tag current_user.twitter_icon %>
  </h2>
<% else %>
  <%= link_to 'Sign In with Twitter', user_twitter_omniauth_authorize_path, method: :post %>
<% end %>

config/routes.rb に以下を追加

root to: 'root#index'

以上です。

動作確認

docker compose up

して

http://localhost:3000/

にアクセスする。

Image from Gyazo

Sign In with Twitter をクリックする

こうなる

Image from Gyazo

Discussion