とある神戸大生の走り書き

主にプログラミングに関して、学んだことを走り書きとして残していきます。

Railsで複数の外部アプリケーションによるログイン機能を実装する。

外部アプリケーションのアカウントを使ったログイン機能を実装することがあったので、振り返りとしてまとめることにしました。

目的

TwitterGithubなどのアカウントでログインできるような機能の実装を行う。

前提

devise gemによってログイン機能を実装していること。

実装方法 

gemのインストール

下記を追記して、bundle installを実装する。

Gemfile

gem 'omniauth'

gem 'omniauth-facebook'

gem 'omniauth-github'

gem 'omniauth-google-oauth2'

gem 'omniauth-linkedin'

gem 'omniauth-mixi'

gem 'omniauth-twitter'

oauth申請

それぞれのアプリケーションでoauthの申請をしましょう!

「{外部アプリケーションの名前} oauth 申請」と検索したら大体分かると思います。

oauth申請でcallback用のURLが必要な場合は、とりあえず、

https://localhost:3000/user/auth/[provider]/callback

としましょう。

一例として、githubでは下記のリンクから申請できます。

github.com

この申請をすることで、AccessKeyとAccessSecretが得られます。

AccessKeyとAccessSecretをアプリ上に追記

下記のように、それぞれのアプリケーションで得られたAccessKeyとAccessSecretをomniauth.ymlに入れる。

config/omniauth.yml

production: &production
  facebook:
    key: xxxxxxxxxxx
    secret: XXXXXXXXXXXXXXXXXXXXXXXXX
  github:
    key: xxxxxxxxxxx
    secret: XXXXXXXXXXXXXXXXXXXXXXXXX
  google:
    key: xxxxxxxxxxx
    secret: XXXXXXXXXXXXXXXXXXXXXXXXX
  hatena:
    key: xxxxxxxxxxx
    secret: XXXXXXXXXXXXXXXXXXXXXXXXX
  linkedin:
    key: xxxxxxxxxxx
    secret: XXXXXXXXXXXXXXXXXXXXXXXXX
  mixi:
    key: xxxxxxxxxxx
    secret: XXXXXXXXXXXXXXXXXXXXXXXXX
  twitter:
    key: xxxxxxxxxxx
    secret: XXXXXXXXXXXXXXXXXXXXXXXXX

development: &development
  <<: *production
  facebook:
    key: xxxxxxxxxxx
    secret: XXXXXXXXXXXXXXXXXXXXXXXXX
  github:
    key: xxxxxxxxxxx
    secret: XXXXXXXXXXXXXXXXXXXXXXXXX
  mixi:
    key: xxxxxxxxxxx
    secret: XXXXXXXXXXXXXXXXXXXXXXXXX

test:
  <<: *development

developmentとproductionで分かれているアプリケーションがあるのは、 申請の際にcallback用のURLを一緒に登録しなければならないために、開発環境と本番環境でそれぞれoauthの申請をしなければならないからです。

(開発環境では、「http://localhost:3000/」で本番環境では、「https://hoge.com/」みたいな感じ)

これらkeyやsecretは、本番環境では暗号化しなければなりません。 暗号化の方法については、下記を参考にすると分かりやすいと思います。

qiita.com

受け取る情報の設定

下記のようにします。

devise.rb

OAUTH_CONFIG = YAML.load_file("#{Rails.root}/config/omniauth.yml")[Rails.env].symbolize_keys!

  # https://github.com/mkdynamic/omniauth-facebook
  # https://developers.facebook.com/docs/concepts/login/
  config.omniauth :facebook, OAUTH_CONFIG[:facebook]['key'], OAUTH_CONFIG[:facebook]['secret'], scope: 'email,publish_stream,user_birthday'

  # https://github.com/intridea/omniauth-github
  # http://developer.github.com/v3/oauth/
  # http://developer.github.com/v3/oauth/#scopes
  config.omniauth :github, OAUTH_CONFIG[:github]['key'], OAUTH_CONFIG[:github]['secret'], scope: 'user,public_repo'

  # https://github.com/zquestz/omniauth-google-oauth2
  # https://developers.google.com/accounts/docs/OAuth2
  # https://developers.google.com/+/api/oauth
  config.omniauth :google_oauth2, 
OAUTH_CONFIG[:google]['key'], OAUTH_CONFIG[:google]['secret'], scope: 'https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/plus.me https://www.google.com/m8/feeds', name: :google

  # https://github.com/mururu/omniauth-hatena
  # http://developer.hatena.ne.jp/ja/documents/auth/apis/oauth
  config.omniauth :hatena, OAUTH_CONFIG[:hatena]['key'], OAUTH_CONFIG[:hatena]['secret']

  # https://github.com/skorks/omniauth-linkedin
  # https://developer.linkedin.com/documents/authentication
  # https://developer.linkedin.com/documents/profile-fields
  config.omniauth :linkedin, OAUTH_CONFIG[:linkedin]['key'], OAUTH_CONFIG[:linkedin]['secret'], scope: 'r_basicprofile r_emailaddress r_network',
    fields: [
      "id", "first-name", "last-name", "formatted-name", "headline", "location", "industry", "summary", "specialties", "positions", "picture-url", "public-profile-url", # in r_basicprofile
      "email-address",  # in r_emailaddress
      "connections"  # in r_network
    ]

  # https://github.com/pivotal-sushi/omniauth-mixi
  # http://developer.mixi.co.jp/connect/mixi_graph_api/api_auth/
  config.omniauth :mixi, OAUTH_CONFIG[:mixi]['key'], OAUTH_CONFIG[:mixi]['secret']

  # https://github.com/arunagw/omniauth-twitter
  # https://dev.twitter.com/docs/api/1.1
  config.omniauth :twitter, OAUTH_CONFIG[:twitter]['key'], OAUTH_CONFIG[:twitter]['secret']

それぞれのアプリケーションで受け取る情報が異なっています。

テーブル

① ターミナルで下記を実行して、SocialProfileテーブルを作成する

rails g model SocialProfile 

② 作成したテーブルのカラムを設定する

①で作成した[作成した日時]_create_social_profiles.rbのchangeメソッドを下記のように編集する。

[作成した日時]_create_social_profiles.rb

  def change
    create_table :social_profiles do |t|
      t.references :user, foreign_key: true
      t.string :provider
      t.string :uid
      t.string :name
      t.string :nickname
      t.string :email
      t.string :url
      t.string :image_url
      t.string :description
      t.text :other
      t.text :credentials
      t.text :raw_info

      t.timestamps
    end
    add_index :social_profiles, [:provider, :uid], unique: true
  end

その後、下記のコマンドを実行する。

rails db:migrate

少し上記のカラムについて説明します。

下記は大体どのアプリケーションでも受け取る情報となるので、必須のカラムです。

  • provider
  • uid
  • email
  • description
  • image_url
  • url

それ以外のカラムは、自分がどの外部アプリケーション認証を導入するかによって少しずつ変化していきます。

どんなカラムを追加すればいいかについては、それぞれの外部アプリケーションアカウントを導入するためのgemのREADME.mdを読めば大体わかるようになっていると思います。 例えば、Twitterであれば、以下のURLのREADME.mdに記載されています。

github.com

モデル

下記のようにsocial_profile.rbに追記します。

social_profile.rb

class SocialProfile < ApplicationRecord
  belongs_to :user
  store :other
  validates_uniqueness_of :uid, scope: :provider

  scope :search_with_providers, ->(provider) { where(provider: provider) }

  def set_values(omniauth)
    return if provider.to_s != omniauth['provider'].to_s || uid != omniauth['uid']
    credentials = omniauth['credentials']
    info = omniauth['info']

    self.access_token = credentials['token']
    self.access_secret = credentials['secret']
    self.credentials = credentials.to_json
    self.email = info['email']
    self.name = info['name']
    self.nickname = info['nickname']
    self.description = info['description'].try(:truncate, 255)
    self.image_url = info['image']
    case provider.to_s
    when 'hatena'
      self.url = "https://www.hatena.ne.jp/#{uid}/"
    when 'github'
      self.url = info['urls']['GitHub']
      self.other[:blog] = info['urls']['Blog']
    when 'google'
      self.nickname ||= info['email'].sub(/(.+)@gmail.com/, '\1')
    when 'linkedin'
      self.url = info['urls']['public_profile']
    when 'mixi'
      self.url = info['urls']['profile']
    when 'twitter'
      self.url = info['urls']['Twitter']
      self.other[:location] = info['location']
      self.other[:website] = info['urls']['Website']
    end

    self.set_values_by_raw_info(omniauth['extra']['raw_info'])
  end

  def set_values_by_raw_info(raw_info)
    case provider.to_s
    when 'google'
      self.url = raw_info['link']
    when 'twitter'
      self.other[:followers_count] = raw_info['followers_count']
      self.other[:friends_count] = raw_info['friends_count']
      self.other[:statuses_count] = raw_info['statuses_count']
    end

    self.raw_info = raw_info.to_json
    self.save!
  end
end

この部分では、それぞれの外部アプリケーションで処理を分けて値を代入しています。 自分が実装したい外部アプリケーション以外の処理の部分だけ追記しましょう。

次に、user.rbを下記のようにします。

user.rb

class User < ActiveRecord::Base
  devise :omniauthable

  has_many :social_profiles, dependent: :destroy
  def social_profile(provider)
    social_profiles.select{ |sp| sp.provider == provider.to_s }.first
  end
end

ルーティング

routes.rbに下記のように追加します。 下記では、コントローラーがusersディレクトリ以下にある場合のルーティングを示しています。自分のディレクトリに変更しましょう。

routes.rb

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

コントローラー

deviseでのログイン機能の導入を前提にしているので、現時点で、下記のコントローラーがある場合もあるかもしれません。

  • confirmations_controller.rb
  • passwords_controller.rb
  • registrations_controller.rb
  • sessions_controller.rb
  • unlocks_controller.rb
  • omniauth_callbacks_controller.rb

もし無い場合は、下記のコマンドで作成されます。

ディレクトリ名は好きなようにで入力しましょう。

rails g devise:controllers [ディレクトリ名]

先ほど作成したomniauth_callbacks_controller.rbに下記のように追記します。

omniauth_callbacks_controller.rb

 def facebook; basic_action; end
 def github; basic_action; end
 def google; basic_action; end
 def hatena; basic_action; end
 def linkedin; basic_action; end
 def mixi; basic_action; end
 def hatena; basic_action; end

private
  def basic_action
    @omniauth = request.env['omniauth.auth']
    if @omniauth.present?
      @profile = SocialProfile.where(provider: @omniauth['provider'], uid: @omniauth['uid']).first
      unless @profile
        @profile = SocialProfile.where(provider: @omniauth['provider'], uid: @omniauth['uid']).new
        @profile.user = current_user || User.create!(username: @omniauth['name'], email: dammy_mail, password: dammy_password)
        @profile.save!
      end
      if current_user
        flash[:notice] = "外部アプリケーションとの連携が完了しました" if current_user == @profile.user
        flash[:alert] = "このアカウントは他のユーザーによって連携されています" if current_user != @profile.user
      else
        sign_in(:user, @profile.user)
      end
      @profile.set_values(@omniauth)
    end
    redirect_to user_path(current_user)
  end

  def dammy_mail
    dammy_mail = "hoge@example.com"
    return dammy_mail
  end

  def dammy_password
    dammy_password = "000000"
    return dammy_password
  end

dammy_mail、dammy_passwordとあるのは、deviseのログイン機能でemailとpasswordに対して存在のバリデーションを設けているからです。

参考

qiita.com

以上で終了です。もし、間違っているところなどがあれば教えていただけるとありがたいです!