このページの内容は以下のリポジトリに1日遅れで反映されます(記事執筆前に前日分をコミットしています)。
https://github.com/hkob/hkob_blog
注意: この日の作業は翌日全てなかったことにしています。順番に見ている人はこの日の作業をスキップしてください。
はじめに
Rails Tips の 74 回目です。Rails ガイドでは BASIC認証をしていますが、せっかくなので Devise を使った認証を使ってみます。ただし、パスワード管理などはしたくないため、Google による OAuth 認証をしてみましょう。
11. セキュリティ
Google による OAuth2 については、以下のページを参考に実装しました。
[Rails]deviseとomniauthによるgoogleログイン
まず、Google Cloud にてプロジェクトを作成します。
その後、「API とサービス」から「OAuth 同意画面」をクリックします。「User Type」を外部とし、「作成」をクリックして、OAuth 2.0 クライアント ID を作成します。
- クライアントID: xxxxxxxxxxxxxxxxxxx
- クライアントシークレット: xxxxxxxxxxxxxxxxxxxxx
- 承認済みのリダイレクトURI: http://localhost:3000/users/auth/google/callback
これらの情報をクレデンシャルファイルに保存します。
EDITOR=vim rails credentials:edit -e development Adding config/credentials/development.key to store the encryption key: 3bda9e36f804268c7fc3c0868395cf5d Save this in a password manager your team can access. If you lose the key, no one, including you, can access anything encrypted with it. create config/credentials/development.key Ignoring config/credentials/development.key so it won't end up in Git history: append .gitignore Configured Git diff driver for credentials. Editing config/credentials/development.yml.enc... File encrypted and saved.
中身は以下のようになります。
google: google_client_id: *** google_client_secret: ***
認証系の gem を追加し、bundle します。
gem "devise" gem "omniauth" gem "omniauth-rails_csrf_protection" gem "omniauth-github" gem "omniauth-google-oauth2"
最初に devise のインストールをを行います。
$ bin/rails g devise:install create config/initializers/devise.rb create config/locales/devise.en.yml =============================================================================== Depending on your application's configuration some manual setup may be required: 1. Ensure you have defined default url options in your environments files. Here is an example of default_url_options appropriate for a development environment in config/environments/development.rb: config.action_mailer.default_url_options = { host: 'localhost', port: 3000 } In production, :host should be set to the actual host of your application. * Required for all applications. * 2. Ensure you have defined root_url to *something* in your config/routes.rb. For example: root to: "home#index" * Not required for API-only Applications * 3. Ensure you have flash messages in app/views/layouts/application.html.erb. For example: <p class="notice"><%= notice %></p> <p class="alert"><%= alert %></p> * Not required for API-only Applications * 4. You can copy Devise views (for customization) to your app by running: rails g devise:views * Not required * ===============================================================================
config/initializers/devise.rb に omniauth の設定を追加します。
config.omniauth :google_oauth2, Rails.application.credentials.google[:google_client_id], Rails.application.credentials.google[:google_client_secret]
モデルは User として作成します。
$ bin/rails g devise User invoke active_record create db/migrate/20240211070843_devise_create_users.rb create app/models/user.rb invoke rspec create spec/models/user_spec.rb insert app/models/user.rb route devise_for :users
初期状態で、user モデルは以下のようになっています。devise の後ろを修正します。
class User < ApplicationRecord # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable end
omniauthable を設定します。ログイン情報を確認したいので、trackable も追加で登録しておきます。
class User < ApplicationRecord devise :omniauthable, omniauth_providers: %i[google_oauth2] devise :trackable validates :uid, uniqueness: {scope: :provider} end
これに合わせて migration ファイルを調整します。name 属性は存在しなかったのですが、追加しておきました。
# frozen_string_literal: true class DeviseCreateUsers < ActiveRecord::Migration[7.1] def change create_table :users do |t| t.string :email, null: false, default: "" t.string :name, null: false, default: "" ## Trackable t.integer :sign_in_count, default: 0, null: false t.datetime :current_sign_in_at t.datetime :last_sign_in_at t.string :current_sign_in_ip t.string :last_sign_in_ip # Omniauthable t.string :provider t.string :uid t.timestamps null: false end add_index :users, :email, unique: true end end
migration しておきます。
$ bin/rails db:migrate == 20240211070843 DeviseCreateUsers: migrating ================================ -- create_table(:users) -> 0.0258s -- add_index(:users, :email, {:unique=>true}) -> 0.0016s == 20240211070843 DeviseCreateUsers: migrated (0.0275s) ======================= $ bin/rails db:migrate RAILS_ENV=test == 20240211070843 DeviseCreateUsers: migrating ================================ -- create_table(:users) -> 0.0091s -- add_index(:users, :email, {:unique=>true}) -> 0.0068s == 20240211070843 DeviseCreateUsers: migrated (0.0160s) =======================
routes.rb に users のルーティング情報が追加されていますが、後ろにコールバック用のコントローラを追加します。
devise_for :users, controllers: {omniauth_callbacks: "users/omniauth_callbacks"}
そのコントローラを作る必要があるので、generator で作成します。私はいつも users/omniauth_callbacks という感じで作成するのですが、:: も使えるんですね。知りませんでした。
$ bin/rails g controller users::omniauth_callbacks create app/controllers/users/omniauth_callbacks_controller.rb invoke haml create app/views/users/omniauth_callbacks invoke rspec create spec/requests/users/omniauth_callbacks_spec.rb $ bin/rails g integration_test users::omniauth_callbacks invoke rspec conflict spec/requests/users/omniauth_callbacks_spec.rb Overwrite /Users/hkob/Library/CloudStorage/Dropbox/rails/hkob_blog/spec/requests/users/omniauth_callbacks_spec.rb? (enter "h" for help) [Ynaqdhm] force spec/requests/users/omniauth_callbacks_spec.rb
このコントローラの中身は以下のようになります。ほとんど Copilot が書いてくれました。
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController skip_before_action :verify_authenticity_token, only: :google_oauth2 def google_oauth2 @user = User.from_omniauth request.env["omniauth.auth"] if @user.persisted? sign_in_and_redirect @user, event: :authentication set_flash_message :notice, :success, kind: "Google" if is_navigational_format? else session["devise.google_data"] = request.env["omniauth.auth"].except :extra redirect_to new_user_registration_url, alert: @user.errors.full_messages.join("\n") end end def failure redirect_to root_path, alert: "Authentication failed, please try again." end private def auth request.env["omniauth.auth"] end end
User.from_omniauth が存在しないとエラーになっていました。models/user.rb に以下のメソッドを追加します。
def self.from_omniauth(auth) find_or_create_by(uid: auth.uid, provider: auth.provider) do |user| user.email = auth.info.email user.name = auth.info.name user.skip_confirmation! end end
ほとんどの OAuth2 の説明は、すでにパスワードによるアカウント認証を作成した上で説明していました。今回は、OAuth2 のみの認証にするので、うまくリダイレクトできないようです。調べたところ、カスタマイズしたリダイレクトを設定すればいいとのことです。
deviseでログイン必須ページ、リダイレクトページ指定 - Qiita
まず、config/initializers/devise.rb に以下の設定を追加します。
config.warden do |manager| manager.failure_app = CustomAuthenticationFailure # manager.intercept_401 = false # manager.default_strategies(scope: :user).unshift :some_external_strategy end
lib/custom_authentication_failure.rb を追加します。
touch lib/custom_authentication_failure.rb
ファイルの中身は以下のように記載しました。
class CustomAuthenticationFailure < Devise::FailureApp def redirect_url user_google_oauth2_omniauth_authorize_path end end
ここまでの実装で実行したところ、画面に以下のようなメッセージが出てきました。
Not found. Authentication passthru.
このメッセージで探したところ以下のページにぶつかりました。
SNS認証におけるNot found. Authentication passthru.エラーについて - Qiita
これに従って、config/initializers/omniauth.rb を作成します。
touch config/initializers/omniauth.rb
内容は以下のようにします。
Rails.application.config.middleware.use OmniAuth::Builder do OmniAuth.config.allowed_request_methods = [:post, :get] end
ここまで実装しましたが、「このアプリが無効なリクエストを送信したため、ログインできません。」となってしまいました。明日、Google に認証に飛ばすためのページを一枚追加したいと思います。
おわりに
今日は時間切れで途中になってしまいました。明日続きを記述します。