Twitter APIの仕様変更に伴い、多くのTwitterログインに依存していたアプリの開発者が苦労しています。
私もその一人で、Twitterログインを実装していたアプリをメールアドレスログインに変更する必要がありました。
今回は、Twitterログインを実装していたアプリにメールアドレスログインを実装する方法を紹介します。
1. 既存のログイン機能
既存のログイン機能は、Twitterログインのみでした。
Twitterログインを実装するには、omniauthというgemを使います。
- omniauth/omniauth: OmniAuth is a flexible authentication system utilizing Rack middleware.
- cookpad/omniauth-rails_csrf_protection: CSRF protection for OmniAuth
- arunagw/omniauth-twitter: OmniAuth strategy for Twitter
# Gemfile
gem 'omniauth'
gem 'omniauth-rails_csrf_protection'
gem 'omniauth-twitter'
# config/initializers/omniauth.rb
Rails.application.config.middleware.use OmniAuth::Builder do
provider :twitter, Rails.application.credentials.dig(:twitter, :api_key), Rails.application.credentials.dig(:twitter, :secret_key)
end
# config/routes.rb
Rails.application.routes.draw do
get '/auth/twitter/callback', to: 'sessions#create'
get '/auth/failure', to: 'sessions#failure'
get '/logout', to: 'sessions#destroy'
end
# app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
def create
unless request.env['omniauth.auth'][:uid]
flash[:warning] = '連携に失敗しました'
redirect_to root_path
end
case request.env['omniauth.auth'][:provider].to_sym
when :twitter
user = User.find_or_create_from_auth_hash!(request.env['omniauth.auth']) # ここの処理は割愛しますが、Twitterのuidをもとにユーザーを検索して、存在しなければ作成します
session[:id] = user.id if user.present?
end
redirect_to root_path
end
def failure
redirect_to root_path
end
def destroy
session[:id] = nil
redirect_to root_path
end
end
userモデルのschemaは以下のようになっています。
# db/schema.rb
create_table "users", force: :cascade do |t|
t.string :uid, null: false
t.string :name
t.string :nickname, null: false
t.string :image, null: false
t.string :description
t.string :email
t.timestamps
end
add_index :users, :uid, unique: true
さて、ここで簡単にメールアドレスログインを実装するという方法を選択するにあたった前提の共有をします。
schemaにメールアドレスのnull制約がないことからもわかる通り、Twitterログインの仕様(というか、Twitterの仕様)として、メールアドレスが取得できないことがあります。
電話番号のみのユーザーもいるようです。
しかしそれではリテンション向上施策をこちらから取りづらい(メールでのpushができない)ため、メールアドレスを取得できない場合は、ユーザーにメールアドレスを入力してもらうように以前から実装していました。
(具体的にはメールアドレスがない場合はログイン後にメールアドレスを登録する画面に遷移するようにしていました)
しかし、メールアドレス入力画面で離脱してしまうユーザーも一定数存在していました。
彼らにももしTwitterログインが使用できなくなった際にもログインしてもらえる方法を検討することもできましたが、そもそもメールアドレスがないユーザーはTwitterログインが使えなくなったとしても、ログインしないユーザーの方が多くなると考え、今回はメールアドレスログインで実装することにしました。
2. メールアドレスログインを実装する
メールアドレスログインを実装する方法として最もメジャーなのは、おそらくdeviseというgemを使う方法だと思います。
しかし、今回既存のデータがある中でのメールアドレスログインの実装ということもあり、deviseのあのごちゃごちゃしたスキーマを既存のデータに適用するのはめんどくさいと考え、deviseを使わずに実装することにしました。
そこで、今回はbcryptというgemを使ってパスワードをハッシュ化して保存する方法を採用しました。
# Gemfile
gem 'bcrypt'
# app/models/user.rb
class User < ApplicationRecord
has_secure_password # 追加
def self.create_first_passwords! # 追加
where(password_digest: nil).find_each do |user|
user.password = SecureRandom.hex(10)
user.save!
end
end
end
# db/schema.rb
create_table "users", force: :cascade do |t|
t.string :uid, null: false
t.string :name
t.string :nickname, null: false
t.string :image, null: false
t.string :description
t.string :email
t.string :password_digest # 追加
t.timestamps
end
add_index :users, :uid, unique: true
# config/routes.rb
Rails.application.routes.draw do
get '/auth/twitter/callback', to: 'sessions#create'
get '/auth/failure', to: 'sessions#failure'
get '/logout', to: 'sessions#destroy'
get '/login', to: 'sessions#new' # 追加
post '/login', to: 'sessions#login' # 追加
end
# app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
def create
unless request.env['omniauth.auth'][:uid]
flash[:warning] = '連携に失敗しました'
redirect_to root_path
end
case request.env['omniauth.auth'][:provider].to_sym
when :twitter
user = User.find_or_create_from_auth_hash!(request.env['omniauth.auth']) # ここの処理は割愛しますが、Twitterのuidをもとにユーザーを検索して、存在しなければ作成します
session[:id] = user.id if user.present?
end
redirect_to root_path
end
def failure
redirect_to root_path
end
def destroy
session[:id] = nil
redirect_to root_path
end
def new
end
def login
user = User.find_by(email: params[:email])
if user&.authenticate(params[:password])
session[:id] = user.id
redirect_to root_path
else
flash[:warning] = 'メールアドレスまたはパスワードが間違っています'
redirect_to login_path
end
end
end
# app/views/sessions/new.html.haml
%h1 メールアドレスログイン
= form_with url: login_path, method: :post do |f|
= f.label :email
= f.email_field :email
= f.label :password
= f.password_field :password
= f.submit 'ログイン'
ポイントは以下の通りです。
- パスワードをハッシュ化して保存するために、
has_secure_password
をモデルに追加する - 初期パスワードを設定するために、
create_first_passwords!
メソッドをUserモデルに追加する - schemafileの
t.string :password_digest
にはnull制約をつけない(初期値として存在しないレコードがすでにあるため)
これで初期値を作成したので、メールアドレスログインができるようになりました。
しかし、create_first_passwords!
で作成した初期パスワードは、ユーザーに伝えられないため、ユーザーはログインできません。
そこで、メールアドレス認証ができればパスワードを変更できる「パスワードを忘れた方はこちら」を実装して最初はそちらからパスワードを作成してもらうことにしました。
ちょっと長くなりすぎそうなので、こちらはまた別の機会に紹介しますね。
今回はこのあたりで。