ログインはメールアドレスでさせておいてログイン後に各種SSOサービスとの連携を済ませる方法を考えてみます。
まず、お手軽にやりたいのでDeviseとOmniauthを使うのは確定です。omniauth-facebook
やomniauth-twitter
などを使うと簡単に連携できますよね。
ところが、よくあるDevise+Omniauthのサンプルを見ると大体ユーザーモデルにOAuthの結果を結びつけていることが多いです。ユーザー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を叩いていろいろな連携を行うことができるようになります。
超参考リンク
id:hirayosさんのOmniAuthで認証機能を作る - RuntimeError