Railsでログインとは別に複数のサービスとの連携を行う方法
移転しました →
ログインはメールアドレスでさせておいてログイン後に各種SSOサービスとの連携を済ませる方法を考えてみます。
まず、お手軽にやりたいのでDeviseとOmniauthを使うのは確定です。omniauth-facebook
やomniauth-twitter
などを使うと簡単に連携できますよね。
ところが、よくあるDevise+Omniauthのサンプルを見ると大体ユーザーモデルにOAuthの結果を結びつけていることが多いです。ユーザー1人に対してサービス1種類が関連づけられるみたいな。
でも複数のサービスと接続したいということもありそうです。というか、実際多くのサービスでログインしたあとで他のサービスとの関連付けを行ったりできます。QiitaとかChatworkとか、Gunosyとかもそうだったかも。
モデルを分けます
ユーザーモデルにサービスと認証したフィールドを持たせるからいけないのであって、ユーザーは独立して存在し認証情報は別モデルとしてユーザーと関連づければ、複数のサービスとの連携も可能です。
複数のサービスを区別なく扱える認証モデルみたいなのを作って、ユーザーモデルと1対多の関係をもてばよさそう。
実はまだ試していないので、今から実装しつつ試してみます。
↑の方法でできました
サンプルアプリケーションはこちらにあります。
全体の流れ
- gemを追加
- omniauthの設定ファイルを作る
- Modelを作る
- Controllerを作る
- Viewを作る
- ブラウザで確認!
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)
ブラウザで確認!
こんな感じになりました。
まとめ
今回作ったAuthモデルには認証によって得ることができる情報うちuid
とprovider
の2つの情報しか残していませんでしたが、ここにアクセストークンなどをいい感じで置いておくことでサービスのAPIを叩いていろいろな連携を行うことができるようになります。