PIYO - Tech & Life -

Railsでログインとは別に複数のサービスとの連携を行う方法

ログインはメールアドレスでさせておいてログイン後に各種SSOサービスとの連携を済ませる方法を考えてみます。

まず、お手軽にやりたいのでDeviseとOmniauthを使うのは確定です。omniauth-facebookomniauth-twitterなどを使うと簡単に連携できますよね。

ところが、よくあるDevise+Omniauthのサンプルを見ると大体ユーザーモデルにOAuthの結果を結びつけていることが多いです。ユーザー1人に対してサービス一種類が関連づけられるみたいな。

でも複数のサービスと接続したいということもありそうです。というか、実際多くのサービスでログインしたあとで他のサービスとの関連付けを行ったりできます。QiitaとかChatworkとか、Gunosyとかもそうだったかも。

モデルを分けます

ユーザーモデルにサービスと認証したフィールドを持たせるからいけないのであって、ユーザーは独立して存在し認証情報は別モデルとしてユーザーと関連づければ、複数のサービスとの連携も可能です。

複数のサービスを区別なく扱える認証モデルみたいなのを作って、ユーザーモデルと1対多の関係をもてばよさそう。

実はまだ試していないので、今から実装しつつ試してみます。

↑の方法でできました

サンプルアプリケーションはこちらにあります。

pi-chan/multi-oauth

全体の流れ

  1. gemを追加
  2. omniauthの設定ファイルを作る
  3. Modelを作る
  4. Controllerを作る
  5. Viewを作る
  6. ブラウザで確認!

gemを追加する

Gemfileに必要なGemを追加します。

# Gemfile
gem 'devise'
gem 'omniauth'
gem 'omniauth-hatena'
gem 'omniauth-github'
gem 'omniauth-twitter'
gem 'figaro'
gem 'haml-rails

ユーザー認証のためのDeviseとOAuth連携のためのomniauth各種は当然。ConsumerKey等を隠すためのfigaroとHamlを入れています。

今回はTwitter、Github、はてなとの連携をしてみました。

omniauthの設定ファイルを作る

omniauth用の設定ファイルでConsumerKey等を指定してあげます。Keyはそれぞれ以下のリンクから発行可能。

# config/initializers/omniauth.rb
Rails.application.config.middleware.use OmniAuth::Builder do
  provider :hatena,
  ENV['HATENA_CONSUMER_KEY'],
  ENV['HATENA_CONSUMER_SECRET'],
  {
    scope: "write_public,read_public,write_private,read_private"
  }

  provider :twitter,
  ENV['TWITTER_CONSUMER_KEY'],
  ENV['TWITTER_CONSUMER_SECRET']

  provider :github,
  ENV['GITHUB_CONSUMER_KEY'],
  ENV['GITHUB_CONSUMER_SECRET']
end

Figaroを使っているので、KeyやSecretはconfig/application.ymlに書きます。

# config/application.yml
development:
  HATENA_CONSUMER_KEY: YOUR_HATENA_KEY
  HATENA_CONSUMER_SECRET: YOUR_HATENA_SECRET
  TWITTER_CONSUMER_KEY: YOUR_TWITTER_KEY
  TWITTER_CONSUMER_SECRET: YOUR_TWITTER_SECRET
  GITHUB_CONSUMER_KEY: YOUR_GITHUB_KEY
  GITHUB_CONSUMER_SECRET: YOUR_GITHUB_SECRET

Modelを作る

続いて、ユーザーモデルと認証モデルを作成します。

$ rails g devise:install
$ rails g devise User # ←コメントで指摘をいただいたので修正しました
$ rails g model Auth uid:string provider:string user_id:integer

ユーザーモデルと認証モデルはこんな感じ。ユーザーモデルは複数のAuthモデルと関連できます。

# user.rb
class User < ActiveRecord::Base
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable, :confirmable,
         :recoverable, :rememberable, :trackable, :validatable

  has_many :auths
end
# auth.rb
class Auth < ActiveRecord::Base
  belongs_to :user
end

Controllerを作る

ログインなどのリンクを置くためのルートページ用コントローラと、認証を取り扱うコントローラを作っておきましょう。

$ rails g controller home index
$ rails g controller auth create destroy

HomeControllerでユーザーが触るページを作ります。ログイン済みの場合は現在連携中のサービスをArrayで持っておき、あとでViewで使用します。

# home_controller.rb
class HomeController < ApplicationController
  def index
    if user_signed_in?
      @providers = current_user.auths.pluck(:provider)
    end
  end
end

AuthControllerでは認証後のcallback先としてのcreateと、連携解除のためのdestroyメソッドを用意します。

# auth_controller.rb
class AuthController < ApplicationController
  before_filter :authenticate_user!
  def create
    auth = request.env["omniauth.auth"]
    uid = auth["uid"]
    provider = auth["provider"]
    unless Auth.find_by_uid_and_provider(uid,provider)
      Auth.create(uid:uid, provider:provider, user_id:current_user.id)
    end
    redirect_to root_url
  end

  def destroy
    provider = params[:provider]
    auth = Auth.find_by_provider_and_user_id(provider,current_user.id)
    auth.destroy
    redirect_to root_url
  end
end

createではauth_hashから認証オブジェクトを生成しユーザーと関連づけ、destroyでは認証オブジェクトを削除します。

routingの設定も必要なので、config/routes.rbは次のようにします。

# config/routes.rb
  root "home#index"
  devise_for :users
  get "/auth/:provider/callback" => "auth#create"
  delete "/auth/destroy/:provider" => 'auth#destroy', as: :destroy_connection

Viewを作る

最後にViewを作ります。

ログインしていない場合はログインリンクとサインアップリンクを、ログイン済みの場合は各種サービスとの連携/連携解除リンクとログアウトボタンを表示します。

さきほどhome#indexで取っておいた@providerはここで使っています。

%h1 multi auth
- if user_signed_in?
  %ul
    %li= link_to "ログアウト", destroy_user_session_path, method: :delete
    - if @providers.include? "twitter"
      %li
        twitterと接続済み
        = link_to "接続解除", destroy_connection_path(:twitter), method: :delete, data:{confirm:"Sure?"}
    - else
      %li= link_to "twitterと接続", "/auth/twitter"
    - if @providers.include? "github"
      %li
        githubと接続済み
        = link_to "接続解除", destroy_connection_path(:github), method: :delete, data:{confirm:"Sure?"}
    - else
      %li= link_to "githubと接続", "/auth/github"
    - if @providers.include? "hatena"
      %li
        hatenaと接続済み
        = link_to "接続解除", destroy_connection_path(:hatena), method: :delete, data:{confirm:"Sure?"}
    - else
      %li= link_to "hatenaと接続", "/auth/hatena"
- else
  %ul
    %li= link_to "ログイン", new_user_session_path
    %li= link_to "サインアップ", new_registration_path(:user)

ブラウザで確認!

こんな感じになりました。

multioauth

まとめ

今回作ったAuthモデルには認証によって得ることができる情報うちuidproviderの2つの情報しか残していませんでしたが、ここにアクセストークンなどをいい感じで置いておくことでサービスのAPIを叩いていろいろな連携を行うことができるようになります。

超参考リンク

id:hirayosさんのOmniAuthで認証機能を作る - RuntimeError