@znz blog

ZnZ の memo のようなもの

devise で通常ログイン出来るユーザーが他の複数サービス連携でログインできるようにした

| Comments

devise でデータベース認証を実装している Rails アプリに Twitter などでもログインできるように 実装を追加してみました。

対象バージョン

  • ruby 2.1.2
  • rails 4.1.4
  • devise 3.2.4
  • omniauth 1.2.1
  • omniauth-facebook 1.6.0
  • omniauth-github 1.1.2
  • omniauth-google-oauth2 0.2.4
  • omniauth-twitter 1.0.1
  • (dotenv 0.11.1) (key と secret の管理)
  • (font-awesome-sass 4.1.0) (view での icon メソッド)

前提条件

  • devise で通常の User モデルにメールアドレスとパスワードによる認証は実装されている。
  • 複数サービスとの連携をするために User モデルには連携サービスの情報は直接持たずに UserAuth モデルに分ける。
  • 連携対象サービスは Facebook, GitHub, Google+, Twitter
  • 連携サービスからの追加情報はメールアドレスなども含めて一切とらずに認証だけに利用する。
  • どの連携サービス (provider) かと識別するための ID だけ保存する。
  • key や secret は環境変数で渡す。 (今回は dotenv を使ったが、 config/secrets.yml 経由でも良さそう)
  • 今回はテストは未実装 (連携部分のテストの書き方をまだ調べていないため)
  • 今回の実装範囲では I18n は使わずにメッセージは日本語固定

実装

今回は以下のように実装を追加すると連携サービスでログインできるようになりました。

Gemfile

OmniAuth と使用するサービスを追加します。

Gemfile
1
2
3
4
5
gem 'omniauth'
gem 'omniauth-facebook'
gem 'omniauth-github'
gem 'omniauth-google-oauth2'
gem 'omniauth-twitter'

initializer 追加

key と secret があれば provider 登録するようにしました。

view で使うために、 登録されている provider の情報の取り方がわからなかったのと 追加で名前や Font Awesome のアイコン名も入れたかったので、 AUTH_PROVIDERS という配列にハッシュを入れるようにしています。

config/initializers/omniauth.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
AUTH_PROVIDERS = []

Rails.application.config.middleware.use OmniAuth::Builder do
  key, secret = ENV['FACEBOOK_KEY'], ENV['FACEBOOK_SECRET']
  if key && secret
    provider :facebook, key, secret
    AUTH_PROVIDERS << {
      provider: :facebook,
      name: 'Facebook',
    }
  end

  key, secret = ENV['GITHUB_CONSUMER_KEY'], ENV['GITHUB_CONSUMER_SECRET']
  if key && secret
    provider :github, key, secret
    AUTH_PROVIDERS << {
      provider: :github,
      name: 'GitHub',
    }
  end

  key, secret = ENV['GOOGLE_CLIENT_ID'], ENV['GOOGLE_CLIENT_SECRET']
  if key && secret
    provider :google_oauth2, key, secret
    AUTH_PROVIDERS << {
      provider: :google_oauth2,
      name: 'Google',
      icon: :google
    }
  end

  key, secret = ENV['TWITTER_CONSUMER_KEY'], ENV['TWITTER_CONSUMER_SECRET']
  if key && secret
    provider :twitter, key, secret
    AUTH_PROVIDERS << {
      provider: :twitter,
      name: 'Twitter',
    }
  end

  AUTH_PROVIDERS.each do |provider|
    provider[:icon] ||= provider[:provider]
  end
end

UserAuth モデル作成

rails g model UserAuth user:references uid:string provider:string で作成します。

モデルクラスは生成されたまま使いましたが、 validation を追加した方が良さそうです。

app/models/user_auth.rb
1
2
3
class UserAuth < ActiveRecord::Base
  belongs_to :user
end

データベースの方は null: false や index を追加しました。

db/migrate/*_create_user_auths.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
class CreateUserAuths < ActiveRecord::Migration
  def change
    create_table :user_auths do |t|
      t.references :user, index: true, null: false
      t.string :uid, null: false
      t.string :provider, null: false

      t.timestamps
    end

    add_index :user_auths, [:uid, :provider], unique: true
  end
end

rake db:migrate

rake db:migrate で反映しておきます。

User クラス側

has_many を追加しておきます。

app/models/user.rb
1
  has_many :user_auths, dependent: :destroy

ルーティング追加

まず、どう使われるのか把握するためにルーティングを追加します。

auth_help は後でプライバシーポリシーなどを書いています。

config/routes.rb
1
2
3
  get '/auth/help' => 'auth#help', as: :auth_help
  get '/auth/:provider/callback' => 'auth#create'
  delete '/auth/destroy/:provider' => 'auth#destroy', as: :destroy_connection

ここには出てきていませんが、 OmniAuth で /auth/:provider がルーティングされるようです。

AuthController 実装

参考サイトの実装例を参考にして AuthController を実装しました。

help は静的な情報を表示するだけなので、実装は空です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# -*- coding: utf-8 -*-
class AuthController < ApplicationController
  skip_before_filter :authenticate_user!

  def create
    auth = request.env['omniauth.auth']
    uid = auth['uid']
    provider = auth['provider']
    auth = UserAuth.where(uid: uid, provider: provider).first
    if auth
      flash[:notice] = "#{provider}でログインしました。"
      sign_in_and_redirect auth.user, event: :authentication
    else
      authenticate_user!
      UserAuth.create!(uid: uid, provider: provider, user_id: current_user.id)
      redirect_to root_url, notice: "#{provider}と連携しました。"
    end
  end

  def destroy
    provider = params.require(:provider)
    authenticate_user!
    auth = UserAuth.where(provider: provider, user_id: current_user.id).first
    auth.destroy if auth
    redirect_to root_url, notice: "#{provider}と連携解除しました。"
  end

  def help
  end
end

まだ連携を登録していない状態で

  • ログアウト状態
  • 連携サービスで認証・連携許可
  • 戻ってきて通常ログイン
  • auth/failure に飛ばされる

ということがおきているのですが、 auth/failure を実装していないので、デフォルトの 404 エラー画面になってしまいます。

view helpers 実装

以前から使っていた link_to_sign_in_or_out の実装も同じファイルに持ってきて、 link_to_provider という汎用的なメソッドを実装しました。 使い方は後述します。

AUTH_PROVIDERS のハッシュはここで使っています。

app/helpers/link_to_auth_helper.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# -*- coding: utf-8 -*-
module LinkToAuthHelper
  def link_to_sign_in_or_out(html_options={})
    if user_signed_in?
      body = icon(:'sign-out') + t(:"devise.shared.links.sign_out", default: "Sign out")
      html_options = { method: :delete }.merge(html_options)
      link_to body, :destroy_user_session, html_options
    else
      body = icon(:'sign-in') + t(:"devise.shared.links.sign_in", default: "Sign in")
      link_to body, :new_user_session, html_options
    end
  end

  def link_to_provider(provider, html_options={})
    if current_user
      if UserAuth.where(user_id: current_user.id, provider: provider[:provider]).exists?
        html_options = { method: :delete }.merge(html_options)
        link_to icon(provider[:icon])+"#{provider[:name]}との連携を解除", destroy_connection_path(provider: provider[:provider]), html_options
      else
        link_to icon(provider[:icon])+"#{provider[:name]}と連携", "/auth/#{provider[:provider]}", html_options
      end
    else
      link_to icon(provider[:icon])+"#{provider[:name]}でログイン", "/auth/#{provider[:provider]}", html_options
    end
  end
end

view に追加

ナビゲーションに以下のように追加しました。 認証連携がないときは従来のログイン・ログアウトだけ表示しています。

AUTH_PROVIDERS に登録されているときだけ連携のリンクを表示するようにしています。

app/views/layouts/_navigation.html.slim
1
2
3
4
5
6
7
8
9
10
    li.dropdown
      a.dropdown-toggle data-toggle="dropdown"
        = icon(:user) + 'ログイン管理'
        b.caret
      ul.dropdown-menu
        li= link_to_sign_in_or_out
        - unless AUTH_PROVIDERS.empty?
          li= link_to icon(:info)+"認証連携のヘルプ", auth_help_path
        - AUTH_PROVIDERS.each do |provider|
          li= link_to_provider(provider)

twitter 連携

Twitter は複数アカウントを使うことも通常の使用範囲として想定されていて、 アプリ専用のアカウントもとりやすいので、 Twitter 連携から試してみました。

https://apps.twitter.com/ から Create New App で作成します。

  • Name: Twitter 全体で一意になるアプリケーションの ID 的にも使われるもの。ツイートの時にツイートしたアプリ名としても埋め込まれるが、今回は認証のみなので、他とぶつからないような名前という以上はこだわらなかった。
  • Description: 認証のときに出てくる説明。
  • Website: たとえば http://app.127.0.0.1.xip.io:3000/ など
  • Callback URL: たとえば http://app.127.0.0.1.xip.io:3000/auth/twitter/callback のように /auth/twitter/callback にする。実サイトなら https にすべき。

作成後には Allow this application to be used to Sign in with Twitter のチェックを入れておきます。 チェックがないと Twitter 連携でのログインがうまくいかないようです。

アイコンや Organization なども必要に応じて変更します。

API keys タブで key と secret を取得します。

  • API key : 環境変数 TWITTER_CONSUMER_KEY に設定
  • API secret : 環境変数 TWITTER_CONSUMER_SECRET に設定

dotenv を使っているので .env に以下のような感じで設定しました。 ランダムな文字列のように見えます。

1
2
TWITTER_CONSUMER_KEY="xxxxXXxxXxXxXXXxXxXXXXxxx"
TWITTER_CONSUMER_SECRET="xxXXxXXxxxxxxXXXXXxXxXXxxXXXxXXxxxxXXxxXxxXXxxXxxx"

動作確認

  • 通常のログインをした状態で「Twitter と連携」で Twitter の許可画面に飛びます。
  • 許可すると「Twitter 側に許可情報」と「UserAuth に連携情報」が保存されます。
  • 「Twitter との連携を解除」で「UserAuth が削除」されます。
  • 「ログアウト」して「Twitter でログイン」すると「ログイン画面」に戻ります。
  • ログインすると auth/failure が 404 エラーになります。ここは後で実装する予定です。
  • 再度ログイン後の画面を直接開きます。
  • 再度「Twitter と連携」で「UserAuth に連携情報」が保存されます。「Twitter 側の許可情報」は古いままです。
  • 「ログアウト」して「Twitter でログイン」でログインできます。
  • https://twitter.com/settings/applications で許可を取り消します。
  • 「ログアウト」して「Twitter でログイン」で Twitter の許可画面に飛びます。
  • 許可すると「Twitter 側に許可情報」が保存されて、「UserAuth」は残っているので、そのままログインできます。

GitHub 連携

GitHub 連携は作成後にすぐに使えるので、アプリケーション作成に使っても良いアカウントがあれば一番簡単です。

https://github.com/settings/applications から Register new application で作成します。

  • Application name: 適切な名前を設定
  • Homepage URL: たとえば http://app.127.0.0.1.xip.io:3000/ など
  • Application description: ユーザーが許可するときにわかりやすい説明
  • Authorization callback URL: たとえば http://app.127.0.0.1.xip.io:3000/auth/github/callback のように /auth/github/callback にする。実サイトなら https にすべき。

右上に見えている Client ID と Client Secret を使います。

  • Client ID : 環境変数 GITHUB_CONSUMER_KEY に設定
  • Client Secret : 環境変数 GITHUB_CONSUMER_SECRET に設定

dotenv を使っているので .env に以下のような感じで設定しました。 十六進数の数字のように見えます。

1
2
GITHUB_CONSUMER_KEY="xxxxxxxxxxxxxxxxxxxx"
GITHUB_CONSUMER_SECRET="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

Facebook 連携

https://developers.facebook.com/ から上の Apps の中にある Craete a New App で作成します。 最初は作成前に開発者関連の規約などに同意する必要があるようです。

  • Display Name: 適切な名前を設定します。
  • Namespace: optional なので空欄のままで良いようです。
  • カテゴリ: 適当に選択します。

セキュリティチェックがあるので、入力すると作成できます。

右上に見えている App ID と App Secret (Show を押すとパスワード認証の後に内容表示) を使います。

  • App ID : 環境変数 FACEBOOK_KEY に設定
  • App Secret : 環境変数 FACEBOOK_SECRET に設定

dotenv を使っているので .env に以下のような感じで設定しました。 十進数と十六進数の数字のように見えます。

1
2
FACEBOOK_KEY="xxxxxxxxxxxxxxx"
FACEBOOK_SECRET="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

SettingsAdd PlatformWebsite を選んで追加します。

  • Site URL: たとえば http://app.127.0.0.1.xip.io:3000/ など
  • Mobile Site URL: 空欄のまま

callback URL は設定が不要だったので、 Site URL の下ならどこでも良さそうです。

google_oauth2 連携

https://github.com/zquestz/omniauth-google-oauth2 に書いてあるように https://code.google.com/apis/console/ を開きます。

飛ばされる先で API & AUTH の中の CredentialsCreate new Client ID で作成します。

  • Application type : Web application
  • Authorized JavaScript origins : たとえば http://app.127.0.0.1.xip.io:3000 など
  • Authorized redirect URI : たとえば http://app.127.0.0.1.xip.io:3000/auth/google_oauth2/callback のように /auth/google_oauth2/callback にする。実サイトなら https にすべき。

右に見えている Client ID と Client secret を使います。

  • Client ID : 環境変数 GOOGLE_CLIENT_ID に設定
  • Client secret : 環境変数 GOOGLE_CLIENT_SECRET に設定

dotenv を使っているので .env に以下のような感じで設定しました。 ドメインっぽい文字列とランダムな文字列のように見えます。

1
2
GOOGLE_CLIENT_ID="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com"
GOOGLE_CLIENT_SECRET="XXxxxXxXxxxXXxXXxXxXXxxX"

さらに Consent screen での設定が必要です。 しかも全アプリ共通のようなので、名前を分けたい場合はグーグルアカウントごと違うものにしないといけないように見えました。

  • Email address : グーグルアカウントのアドレス選択
  • Product name : 適切な名前を設定
  • Homepage URL 以下 : 必要に応じて設定

さらにエラーメッセージ ("Access Not Configured. Please use Google Developers Console to activate the API for your project.") で検索してわかったのですが、 Google OAuth 2.0 loginが2014年9月に使えなくなる(Googleアカウントからのユーザ登録機能がある場合は注意) に書いてあるように、 APIs で Google+ API も有効にする必要がありました。

参考サイト

Comments