「Facebookアカウントでログイン」機能をつくる(Rails3, Devise, OmniAuth, Mongoid)

はじめに

巷のWebアプリでよく見かける「Facebookアカウントでログイン」「Twitterアカウントでログイン」のボタン。ユーザー認証の必要があるWebサービスでも、メールアドレスやパスワード設定の手間が省けてとっても便利だ。これはOAuthと言って、FacebookTwitterといった外部サービス(認証プロバイダ)が提供するAPIを利用することで実現されている。

今回はRails3で上記の「Facebookアカウントでログイン」機能を実現する手順についてまとめてみた。

※なお、「マイグレーションが無くて楽」という理由でRailsのORMにはActiveRecordではなくMongoidを利用している。

環境

使用するライブラリ

  • Mongoid: Ruby用MongoDBのORM。MongoDBのORMとしては他にMongoMapperというものもあるが、こちらの方がドキュメントが充実していて人気の模様。
  • Devise: 言わずと知れたRails3デファクトの認証用ライブラリ。
  • OmniAuth: RubyでOAuth認証を実現するためのライブラリ。Deviseと連携可能。

MongoDBのインストール

homebrewが入っていればMongoDBのインストールは簡単。

$ brew install mongodb

で一発。インストールが済んだら、とりあえず

$ mongod &

でバックグラウンド起動しておく。
本当はサービスとしてOS起動時に立ち上がるよう設定したいけど今はこれで 十分。

Devise, Mongoidインストール済みRailsアプリの作成

世の中には徳のある御仁がいて、yes/noの質問に答えるだけでDeviseとMongoidの設定済みRailsアプリを生成してくれるテンプレートがgithubで公開されている。今回はこれを利用させて頂く。
RailsApps/rails3-mongoid-devise · GitHub
テンプレートからRailsアプリを生成するには-mオプションでテンプレートファイルを指定する:

$ rails new my_auth_test_app -m https://github.com/RailsApps/rails3-application-templates/raw/master/rails3-mongoid-devise-template.rb -T -O

コマンドを実行するとウィザードがアプリで使用するテンプレートエンジンやテスティングフレームワークなどの項目を尋ねてくるので、以下のように返答した:

      wizard  You are using Rails version 3.2.6.
      wizard  Checking configuration. Please confirm your preferences.
      insert    config/application.rb
      recipe  Running HAML recipe...
        haml  Would you like to use Haml instead of ERB? (y/n) y
y
     gemfile    haml (>= 3.1.6)
     gemfile    haml-rails (>= 0.3.4)
      recipe  Running RSpec recipe...
       rspec  Would you like to use RSpec instead of TestUnit? (y/n) y
y
    question  Which library would you like to use for test fixtures with RSpec?
          1)  None
          2)  factory_girl
          3)  machinist
       rspec  Enter your selection: 2
2
     gemfile    rspec-rails (>= 2.11.0)
     gemfile    database_cleaner (>= 0.8.0)
     gemfile    mongoid-rspec (1.4.6)
     gemfile    factory_girl_rails (>= 3.5.0)
     gemfile    email_spec (>= 1.2.1)
      create    features/support/email_spec.rb
      recipe  Running Cucumber recipe...
    cucumber  Would you like to use Cucumber for your BDD? (y/n) y
y
     gemfile    cucumber-rails (>= 1.3.0)
     gemfile    capybara (>= 1.1.2)
     gemfile    database_cleaner (>= 0.8.0)
     gemfile    launchy (>= 2.1.0)
      recipe  Running guard recipe...
    question  Would you like to use Guard to automate your workflow?
          1)  No
          2)  Guard default configuration
          3)  Guard with LiveReload
       guard  Enter your selection: 1
1
       guard  Guard recipe skipped.
      recipe  Running Mongoid recipe...
     mongoid  Would you like to use Mongoid to connect to a MongoDB database? (y/n) y
y
     mongoid  REMINDER: When creating a Rails app using Mongoid...
     mongoid  you should add the '-O' flag to 'rails new'
     gemfile    mongoid (>= 3.0.1)
      recipe  Running ActionMailer recipe...
    question  How will you send email?
          1)  SMTP account
          2)  Gmail account
          3)  SendGrid account
          4)  Mandrill by MailChimp account
  action_mailer  Enter your selection: 1
1
      recipe  Running Devise recipe...
    question  Would you like to use Devise for authentication?
          1)  No
          2)  Devise with default modules
          3)  Devise with Confirmable module
          4)  Devise with Confirmable and Invitable modules
      devise  Enter your selection: 2
2
      devise  Would you like to manage authorization with CanCan & Rolify? (y/n) n
n
     gemfile    devise (>= 2.1.2)
      recipe  Running AddUser recipe...
      recipe  Running HomePage recipe...
      recipe  Running HomePageUsers recipe...
      recipe  Running SeedDatabase recipe...
      recipe  Running UsersPage recipe...
      recipe  Running html5 recipe...
    question  Which front-end framework would you like for HTML5 and CSS?
          1)  None
          2)  Zurb Foundation
          3)  Twitter Bootstrap (less)
          4)  Twitter Bootstrap (sass)
          5)  Skeleton
          6)  Just normalize CSS for consistent styling
       html5  Enter your selection: 1
1
      recipe  Running SimpleForm recipe...
    question  Which form gem would you like?
          1)  None
          2)  simple form
          3)  simple form (bootstrap)
  simple_form  Enter your selection: 1
1
  simple_form  Form help recipe skipped.
      recipe  Running Cleanup recipe...
      recipe  Running Extras recipe...
      extras  Would you like to use 'rails-footnotes' (it's SLOW!)? (y/n) n
n
      extras  Would you like to set a robots.txt file to ban spiders? (y/n) y
y
      extras  Would you like to add 'will_paginate' for pagination? (y/n) n
n
      extras  Add 'therubyracer' JavaScript runtime (for Linux users without node.js)? (y/n) n
n
      extras  Banning spiders by modifying 'public/robots.txt'
      recipe  Running Git recipe...
      wizard  Running 'bundle install'. This will take a while.

これでDeviceとMongoidのインストール済みアプリが出来上がった。おまけにfactory_girlもrspecもhamlも導入済み。超ラク。ところでCanCanとRollifyってよく知らないけど何だろう。

mongoidの設定

MongoDBへの接続設定ファイルをジェネレーターで作成する:

rails g mongoid:config

これでconfig/mongoid.yml というデフォルト状態の設定ファイルができあがる。コメントを抜かした中身はこんな感じ:

# config/mongoid.yml
development:
  sessions:
    default:
      database: my_auth_test_app_development
      hosts:
        - localhost:27017
      options:
        consistency: :strong
  options:
test:
  sessions:
    default:
      database: my_auth_test_app_test
      hosts:
        - localhost:27017
      options:
        consistency: :strong

サンプルアプリだし特に変更する必要も無いので今回はデフォルトのままにしておく。
mongodbの便利な所は、事前に上記データベースのcreateを実行しておく必要がないところ。初回のデータinsert時にデータベースが自動で作成される。

OmniAuthとDeviseの連携

DBの設定も完了してRailsアプリの土台は出来上がった。ここからOmniAuthとDeviseを連携させてFacebook経由でユーザー認証を行う仕組みを作っていく。連携と言ってもDeviseはver 1.2からOmniAuthによる認証をサポートしていて、OmniAuthの設定はすべてDevise側から行うことができる。

Facebook側の設定

Facebookを認証プロバイダーとして利用するためには、まずFacebook開発者サイトでアプリの登録を行っておく必要がある。
新しくアプリを作成するとアプリIDとシークレットキーが与えられるので、これをメモっとく(あとで必要)。
またアプリのサイトURLの設定も必要。今回はローカルのみで動かすので "http://localhost:3000/"とだけ設定しておいた。

OmniAuthのインストール

Gemfileに以下を追記して bundle install。

gem "omniauth"
gem "omniauth-facebook"

Userモデルの作成

そもそもの認証の対象となるUserモデルのひな形を作成しておく。その前にdeviseの初期化も実行しておかないといけない。

$ rails g devise:install
$ rails g devise User

ジェネレータで作成されたuser.rbを編集し、UserがOmniAuthの対象であることをDeviseに教えてやる。また認証情報を保持するため、Userモデルのひな形にはproviderとuidというフィールドを加えておく:

  devise :omniauthable
  field :provider, :type => String # facebook等の認証プロバイダ
  field :uid,      :type => String # 認証プロバイダ内のユーザーID

ここでrake routesを実行すると、ユーザーのomniauth用名前付きパスが作成されていることが分かる:

 user_omniauth_authorize        /users/auth/:provider(.:format)        devise/omniauth_callbacks#passthru {:provider=>/facebook/}
  user_omniauth_callback        /users/auth/:action/callback(.:format) devise/omniauth_callbacks#(?-mix:facebook)

サーバーを立ち上げhttp://localhost:3000/users/auth/facebookにアクセスすると、Facebookの認証用ページにリダイレクトされる:
f:id:yhashy:20120721160833p:plain
このページでユーザーが「アプリへ移動」を選択すると、認証情報が/users/auth/facebook/callbackにコールバックされる、という寸法。あとはFBからのコールバックを受け取る処理を加えてあげればよい。

Facebookからのコールバックを処理する

config/routes.rbを編集し、omniauthのコールバック用コントローラーを下記の通り変更する:

devise_for :users, :controllers => { :omniauth_callbacks => "users/omniauth_callbacks" }

コールバックの処理は次のように実装する:

class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
  def facebook
    @user = User.find_for_facebook_oauth(request.env["omniauth.auth"], current_user)
    if @user.persisted?
      flash[:notice] = "Athentication successful!"
      sign_in(:user, @user)
      redirect_to root_path
    else
      session["devise.facebook_data"] = request.env["omniauth.auth"]
      redirect_to new_user_registration_url
    end
  end
end

Facebookから送られる認証情報はrequest.env["omniauth.auth"]に渡されるため、これを使ってユーザーの検索/作成をしている。

Userモデルでは find_for_facebook_oauthを次のように実装する:

  def self.find_for_facebook_oauth(auth, signed_in_resource=nil)
    user = User.where(:provider => auth.provider, :uid => auth.uid).first
    unless user
      user = User.create(name: auth.extra.raw_info.name,
                         provider: auth.provider,
                         uid: auth.uid,
                         email: auth.info.email,
                         password: Devise.friendly_token[0,20]
                         )
    end
    user
  end

ちなみにomniauthを使用する場合ユーザーがパスワードを入力する必要がないので、passwordフィールドにはDevise#friendly_tokenで生成したランダム文字列を渡している。

Viewを整える

整えるっていうか動作確認のためにリンク作るだけだけど...
Homeコントローラーを作成して、rootパスをhome#indexアクションに割り当て、index.html.hamlに次のように記述してやる。

%h1 Home#index
%p Find me in app/views/home/index.html.haml
- if !user_signed_in?
  = link_to "Facebookアカウントでログイン", user_omniauth_authorize_path(:facebook)
- else
  = "#{current_user.name}としてログインしています."
  = link_to "ログアウト", destroy_user_session_path, :method => :delete

動作確認

FBの開発者サイトでテスト用のダミーユーザーを作成した。その名もミスBharambeman。彼女の疑似アカウントでFBにログインし、http://localhost:3000/にアクセスすると
f:id:yhashy:20120722213736p:plain
Facebookアカウントでログイン」のリンクを踏み、FBの認証用ページに移動する:
f:id:yhashy:20120722213801p:plain
ここで「アプリへ移動」を選択してやれば、
f:id:yhashy:20120722213914p:plain
Facebookのユーザー情報からユーザーが作成されたことが分かる。

課題

  • Twitter, OpenIDFacebook以外の認証プロバイダにも対応する(これは簡単だけど)。
  • 上記と関連して、1ユーザーに対して複数の認証情報を紐づけられるようにする(Userモデルと認証情報を切り離す)。
  • Viewをもうちょっとカッコ良くする(プロフィール写真表示するとか)
  • もっと簡潔に記事を書く。