[Rails] omniauthしかログイン手段がなかったアプリにメールアドレスログインを実装する

2023/04/18

Rails
omniauth
Twitter

Twitter APIの仕様変更に伴い、多くのTwitterログインに依存していたアプリの開発者が苦労しています。

私もその一人で、Twitterログインを実装していたアプリをメールアドレスログインに変更する必要がありました。

今回は、Twitterログインを実装していたアプリにメールアドレスログインを実装する方法を紹介します。

1. 既存のログイン機能

既存のログイン機能は、Twitterログインのみでした。

Twitterログインを実装するには、omniauthというgemを使います。

Gemfile
# Gemfile
gem 'omniauth'
gem 'omniauth-rails_csrf_protection'
gem 'omniauth-twitter'
config/initializers/omniauth.rb
# 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
# 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
# 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
# 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
# Gemfile
gem 'bcrypt'
app/models/user.rb
# 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
# 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
# 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
# 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
# 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 'ログイン'

ポイントは以下の通りです。

  1. パスワードをハッシュ化して保存するために、has_secure_passwordをモデルに追加する
  2. 初期パスワードを設定するために、create_first_passwords!メソッドをUserモデルに追加する
  3. schemafileのt.string :password_digestにはnull制約をつけない(初期値として存在しないレコードがすでにあるため)

これで初期値を作成したので、メールアドレスログインができるようになりました。
しかし、create_first_passwords!で作成した初期パスワードは、ユーザーに伝えられないため、ユーザーはログインできません。

そこで、メールアドレス認証ができればパスワードを変更できる「パスワードを忘れた方はこちら」を実装して最初はそちらからパスワードを作成してもらうことにしました。

ちょっと長くなりすぎそうなので、こちらはまた別の機会に紹介しますね。

今回はこのあたりで。