Rails API(devise_token_auth)+Reactでのログイン時のメール本認証(comfirmable)実装
やったこと
- サインアップページのフォームにメールアドレスとパスワードを入力
- サインアップページの submit ボタンをクリック
- 入力したメールアドレスにメールが届く(仮認証)
- そのメール内のリンクをクリック(本認証)
- ログインできるようになる
実装手順
Backend
- プロジェクト作成
- Devise と HTTP 通信用の gem を追加
- devise と devise_token_auth をインストール
- ログイン機能の設定を変更
- API コントローラー作成
- ルーティング設定
- confirmable を有効にする
- .env ファイルを設定
- gmail のメールアプリパスワードを取得
- メールの設定を追加
- API 通信確認
Frontend
- プロジェクト作成
- 必要パッケージをインストール
- API 設定ファイル作成
- コンポーネント作成
- ログイン状態と非ログイン状態でページを切り分ける
- サインアップページ作成
- サインインページ作成
API(Rails)
1. プロジェクト作成
$ rails new api --api
$ cd api
-
Rails のプロジェクト作成をします。api の部分はプロジェクト名になる。
-
--api
→ API モードで Rails アプリを作成するためのオプションコマンド
2. Devise と HTTP 通信用の gem を追加
# 追加
# Devise
gem 'devise'
gem 'devise_token_auth'
# HTTP通信
gem 'rack-cors'
devise
devise_token_auth
これは、API 通信でログイン機能を実装するために必要になる。
rack-cors
これで、HTTP 通信許可の設定ができる。この gem を追加して、HTTP 通信の設定をすることで、React からのリクエストを許可することができる。
gem をインストールする。
$ bundle install
3. devise と devise_token_auth をインストール
devise_token_auth を設定していく前に、devise をインストールしておく。手順が逆になるとエラーが出る可能性がある。
- devise のインストール
$ rails g devise:install
上のコマンドを実行すると下記のような表示が返ってくる。
create config/initializers/devise.rb
create config/locales/devise.en.yml
===============================================================================
Some setup you must do manually if you haven't yet:
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.
2. Ensure you have defined root_url to *something* in your config/routes.rb.
For example:
root to: "home#index"
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>
4. You can copy Devise views (for customization) to your app by running:
rails g devise:views
===============================================================================
- devise_token_auth のインストール
$ rails g devise_token_auth:install User auth
rails g devise_token_auth:install User auth コマンドで作成された db/migrate/日付_devise_token_auth_create_users.rb
ファイルに示された users テーブルを作成する。
class DeviseTokenAuthCreateUsers < ActiveRecord::Migration[6.1]
def change
create_table(:users) do |t|
## Required
t.string :provider, :null => false, :default => "email"
t.string :uid, :null => false, :default => ""
## Database authenticatable
t.string :encrypted_password, :null => false, :default => ""
## Recoverable
t.string :reset_password_token
t.datetime :reset_password_sent_at
t.boolean :allow_password_change, :default => false
## Rememberable
t.datetime :remember_created_at
## Confirmable
t.string :confirmation_token
t.datetime :confirmed_at
t.datetime :confirmation_sent_at
t.string :unconfirmed_email # Only if using reconfirmable
## Lockable
# t.integer :failed_attempts, :default => 0, :null => false # Only if lock strategy is :failed_attempts
# t.string :unlock_token # Only if unlock strategy is :email or :both
# t.datetime :locked_at
## User Info
t.string :name
t.string :nickname
t.string :image
t.string :email
## Tokens
t.text :tokens
t.timestamps
end
add_index :users, :email, unique: true
add_index :users, [:uid, :provider], unique: true
add_index :users, :reset_password_token, unique: true
add_index :users, :confirmation_token, unique: true
# add_index :users, :unlock_token, unique: true
end
end
migrate コマンドで、users テーブルを作成する。
$ rails db:migrate
4. ログイン機能の設定を変更
まずはログイン時に token を渡してログイン状態を維持するため、token 情報の設定をする。
devise_token_auth.rb を開き、下記の設定を変更する
config.change_headers_on_each_request = false # コメントを外してtrueからfalseに変更する
config.token_lifespan = 2.weeks # コメントをはずす
# コメントを外す
config.headers_names = {:'access-token' => 'access-token',
:'client' => 'client',
:'expiry' => 'expiry',
:'uid' => 'uid',
:'token-type' => 'token-type' }
-
config.change_headers_on_each_request → true の場合、リクエストごとに token を新しくする必要がある、という設定になる。毎回、毎回、トークンが変更されるという動きは今回は期待していないので、false に変更。
-
token_lifespan → 名前の通り、token の有効期間。今回は、2週間に設定。
-
headers_names → 認証用ヘッダーの名前の定義。必要でない限り、変更する必要はない。
次に、application_controller.rb に設定を追加する。(devise_token_auth をインストールした際に自動で追加されているかも)
class ApplicationController < ActionController::API
# 下記一行を追加
include DeviseTokenAuth::Concerns::SetUserByToken
end
- この一文で、
SetUserByToken
という拡張機能を使える様になる。これは、Cookie や CORS の設定ができるようになるということ。
次に application.rb に Cookie や CORS の設定をしていく。
require_relative 'boot'
require "rails"
# Pick the frameworks you want:
require "active_model/railtie"
require "active_job/railtie"
require "active_record/railtie"
require "active_storage/engine"
require "action_controller/railtie"
require "action_mailer/railtie"
require "action_view/railtie"
require "action_cable/engine"
# require "sprockets/railtie"
# require "rails/test_unit/railtie"
# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)
module DeviseTokenAuthTwitter
class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
config.load_defaults 5.2
# Settings in config/environments/* take precedence over those specified here.
# Application configuration can go into files in config/initializers
# -- all .rb files in that directory are automatically loaded after loading
# the framework and any gems in your application.
# Only loads a smaller set of middleware suitable for API only apps.
# Middleware like session, flash, cookies can be added back manually.
# Skip views, helpers and assets when generating a new resource.
config.api_only = true
# ここからコピペする
config.session_store :cookie_store, key: '_interslice_session'
config.middleware.use ActionDispatch::Cookies # Required for all session management
config.middleware.use ActionDispatch::Session::CookieStore, config.session_options
config.middleware.use ActionDispatch::Flash
config.middleware.insert_before 0, Rack::Cors do
allow do
# 今回はRailsのポートが3001番、Reactのポートが3000番にするので、Reactのリクエストを許可するためにlocalhost:3000を設定
origins 'localhost:3000'
resource '*',
:headers => :any,
# この一文で、渡される、'access-token'、'uid'、'client'というheaders情報を用いてログイン状態を維持する。
:expose => ['access-token', 'expiry', 'token-type', 'uid', 'client'],
:methods => [:get, :post, :options, :delete, :put]
end
end
# ここまで
end
end
最後に Rails のポート番号がデフォルトで 3000 番になっているのを 3001 番に変更しておく。
# 3000→3001に修正
port ENV.fetch("PORT") { 3001 }
5. API コントローラー作成
今回はログイン機能の API とログインユーザー情報取得 API のコントローラーを作成していく。
$ rails g controller auth/registrations
$ rails g controller auth/sessions
app/controllers/auth/registrations_controller.rb
はサインアップ、サインイン、サインアウトを実行する API コントローラー
# Auth::RegistrationsControllerクラスはDeviseTokenAuth::RegistrationsControllerを継承する
class Auth::RegistrationsController < DeviseTokenAuth::RegistrationsController
private
def sign_up_params
# サインアップ時に登録できるカラムを指定
params.permit(:email, :password, :password_confirmation)
end
end
app/controllers/auth/sessions_controller.rb
はログイン時のユーザーを取得するための API コントローラー
class Auth::SessionsController < ApplicationController
def index
if current_user
render json: {is_login: true, data: current_user }
else
render json: {is_login: false, message: "ユーザーが存在しません"}
end
end
end
6. ルーティング設定
作成したコントローラーを基に、ルーティングを設定していく。
Rails.application.routes.draw do
# ログイン機能のルーティング
mount_devise_token_auth_for 'User', at: 'auth', controllers: {
registrations: 'auth/registrations'
}
# ログインユーザー取得のルーティング
namespace :auth do
resources :sessions, only: %i[index]
end
end
ルーティングを確認する
$ rails routes
設定したルーティングから考えられる API エンドポイント
- サインアップ →
http://localhost:3001/auth
- サインイン →
http://localhost:3001/auth/sign_in
- サインアウト →
http://localhost:3001/auth/sign_out
- ログインユーザー取得 →
http://localhost:3001/auth/sessions
7. confirmable を有効にする
Confirmable の設定をしていく。
- confirmable を設定すると、ユーザが新たにアカウント登録すると、登録したメールアドレスにメールが送信され、そのメール内の「アカウント確認」を押すまで本登録が完了しない様にできる。
User テーブルで confirmable を有効にする。
class User < ActiveRecord::Base
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable, :confirmable # ← confirmableを追加する
include DeviseTokenAuth::Concerns::User
end
これを追加することで、confirmable を有効にできる。
これは users テーブルを作成するさいに、migration ファイルにあった、confirmable と書いてある部分が有効になるということ。
## Confirmable
t.string :confirmation_token
t.datetime :confirmed_at
t.datetime :confirmation_sent_at
t.string :unconfirmed_email # Only if using reconfirmable
8. gmail のメールアプリパスワードを取得
Action Mailer で G メールを利用するには、アプリパスワードを取得する必要がある。
Gmail で2段階認証をオンにし、アプリパスワードを取得する。
取得したパスワードは環境変数に設定するため控えておく。
9. .env ファイルを設定
Rails プロジェクトで env ファイルを使用する際は、以下の gem を追加する必要があります。
group :development, :test do
# Call 'byebug' anywhere in the code to stop execution and get a debugger console
gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
gem 'dotenv-rails' # 追加する
end
$ bundle install
.env
ファイルを作成する。
$ touch .env
この env ファイルを隠しファイルにするために、.gitignore
ファイルに.env
ファイルを追加する。
# 追加
/.env
これで隠しファイルになり、外からは見られなくなった。
そして、Action Mailer で Gmail を利用するための環境変数を.env
ファイルに記述していく。
EMAIL_ADDRESS=<送信元になる自分のメールアドレス>
EMAIL_PASSWORD=<生成されたアプリのパスワード>
10. メールの設定を追加
devise.rb
に mailer の設定を追加
Devise.setup do |config|
config.mailer_sender = ENV['EMAIL_ADDRESS'] # コメントアウトをはずして変更
config.mailer = 'Devise::Mailer' # コメントアウトを外す
end
次に、config/environments/development.rb
に下記の設定を追加
Rails.application.configure do
# 下記を追加
config.action_mailer.default_options = { from: ENV['EMAIL_ADDRESS'] }
# hostにはデフォルトでlocalhost3000になっているので、Railsのポート番号である3001に変更する。
config.action_mailer.default_url_options = { host: 'localhost:3001' }
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
address: 'smtp.gmail.com',
port: 587,
domain: 'gmail.com',
user_name: ENV['EMAIL_ADDRESS'],
password: ENV['EMAIL_PASSWORD'],
authentication: 'plain',
enable_starttls_auto: true
}
end
11. API 通信確認
ポストマンなどで API 通信の確認をしてみる。
POST http://localhost:3001/auth
サインアップ →{
"email": "送信先のメールアドレス",
"password": "パスワード",
"password_confirmation": "パスワード確認",
"confirm_success_url": "メール内のリンクからのリダイレクト先のURL"
}
-
email
→ 送信先のメールアドレスを設定(サインアップ時に入力した際にそのメールアドレスに認証メールが届く) -
confirm_success_url
→ 本認証が完了した際にリダイレクトする URL を指定する。今回のアプリではログインページに遷移するようにするが、フロントはこれから作るので、API 確認の際は適当にhttps://google.com
などと入力しておけばいい。ちゃんとそのページに遷移したら成功ということになる。
POST http://localhost:3001/auth/sign_in
サインイン →{
"email": "送信先のメールアドレス",
"password": "パスワード",
}
- ちゃんとメール内のリンクからリダイレクト先に遷移したら、サインアップ時に指定したメールアドレスとパスワードを指定して、ログインできる。
DELETE http://localhost:3001/auth/sign_out
サインアウト →- Headers の中に、サインアップ、サインイン時にヘッダー情報に入っていた
access-token、client、uid
情報を入力する。
GET http://localhost:3001/auth/sessions
ログインユーザー取得 →- サインアウト時と同様、サインアップ、サインイン時にヘッダー情報に入っていた
access-token、client、uid
情報を入力する。
Frontend(React)
1. プロジェクト作成
まずはルートディレクトリに移動して、create-react-app
で React プロジェクト作成する。
$ cd my_app
$ npx create-react-app frontend
$ cd frontend
2. 必要パッケージをインストール
今回のログイン機能に必要なパッケージをインストールしていきます。
$ npm i axios axios-case-converter react-router-dom@5.2.0 js-cookie
- axios→HTTP 通信
- axios-case-converter→axios で受け取ったレスポンスの値をスネークケース → キャメルケースに変換、または送信するリクエストの値をキャメルケース → スネークケースに変換してくれるライブラリ
- react-router-dom→ ルーティング設定用のライブラリ
- js-cookie→Cookie を操作するためのライブラリ
axios-case-converter を使う理由は、Rails は Ruby なので、スネークケース(例:password_confirmation)であり、React は JavaScript なので、キャメルケース(例:passwordConfirmation)であり、API で送られてきた Rails 側のデータと React 側のデータの記述を合わせるため。
Cookie を使う理由は、Cookie を操作してaccess-token、client、uid
を渡すため。
3. API 設定ファイル作成
API 設定ファイルを作成します。
$ mkdir src/api
$ touch src/api/client.js
$ touch src/api/auth.js
client.js
でルートエンドポイントを設定する。
import applyCaseMiddleware from 'axios-case-converter'
import axios from 'axios'
const options = {
ignoreHeaders: true,
}
const client = applyCaseMiddleware(
axios.create({
baseURL: 'http://localhost:3001',
}),
options
);
export default client;
auth.js
で、client.js で作成した client を使って、これまで Rails で作成してきた API を設定していく。
import client from "./client";
import Cookies from "js-cookie";
// サインアップ
export const signUp = (params) => {
return client.post("/auth", params);
};
// サインイン
export const signIn = (params) => {
return client.post("/auth/sign_in", params);
};
// サインアウト
export const signOut = () => {
return client.delete("/auth/sign_out", {
headers: {
"access-token": Cookies.get("_access_token"),
client: Cookies.get("_client"),
uid: Cookies.get("_uid"),
},
});
};
// ログインユーザーの取得
export const getCurrentUser = () => {
if (
!Cookies.get("_access_token") ||
!Cookies.get("_client") ||
!Cookies.get("_uid")
)
return;
return client.get("/auth/sessions", {
headers: {
"access-token": Cookies.get("_access_token"),
client: Cookies.get("_client"),
uid: Cookies.get("_uid"),
},
});
};
4. コンポーネント作成
-
ログイン時と非ログイン時を判断するためのコンポーネントを作成する。
-
今回は、ログイン時は Home コンポーネントに遷移するようにして、SignUp コンポーネントと SignIn コンポーネントが非ログイン時として作成していく。
export const Home = () => {
return <p>Homeページです</p>
}
export const SignUp = () => {
return <p>SignUpページです</p>
}
export const SignIn = () => {
return <p>SignInページです</p>
}
5. ログイン状態と非ログイン状態でページを切り分ける
-
それでは、実際にログイン時は Home コンポーネント、非ログイン時は SignUp、SignIn コンポーネントにを表示するようにしていく。
-
App.jsx でそれぞれのコンポーネントのルーティングを作成して、そのルーティングをログイン時と非ログイン時で切り分けていく。
import { createContext, useEffect, useState } from "react";
import { BrowserRouter, Switch, Route, Redirect } from "react-router-dom";
import { getCurrentUser } from "./api/auth";
import { Home } from "./components/Home";
import { SignIn } from "./components/SignIn";
import { SignUp } from "./components/SignUp";
export const AuthContext = createContext();
function App() {
const [loading, setLoading] = useState(true);
const [isSignedIn, setIsSignedIn] = useState(false);
const [currentUser, setCurrentUser] = useState();
const handleGetCurrentUser = async () => {
try {
const res = await getCurrentUser();
if (res?.data.isLogin === true) {
setIsSignedIn(true);
setCurrentUser(res?.data.data);
console.log(res?.data.data);
} else {
console.log("no current user");
}
} catch (e) {
console.log(e);
}
setLoading(false);
};
useEffect(() => {
handleGetCurrentUser();
}, [setCurrentUser]);
const Private = ({ children }) => {
if (!loading) {
if (isSignedIn) {
return children;
} else {
return <Redirect to="signin" />;
}
} else {
return <></>;
}
};
return (
<AuthContext.Provider
value={{
loading,
setLoading,
isSignedIn,
setIsSignedIn,
currentUser,
setCurrentUser,
}}
>
<BrowserRouter>
<Switch>
<Route exact path="/signup">
<SignUp />
</Route>
<Route exact path="/signin">
<SignIn />
</Route>
<Private>
<Route exact path="/">
<Home />
</Route>
</Private>
</Switch>
</BrowserRouter>
</AuthContext.Provider>
);
}
export default App;
6. サインアップページ作成
-
ここでは、サインアップページを作成して、Submit ボタンをクリックしたら本認証するためのメールが、入力したメールアドレスに送信されるようになる。
-
送信されたかがコンソールを見ないとわからないため、成功したら「confirm email」というアラートを表示するようにしている。
import Cookies from "js-cookie";
import { useContext, useState } from "react";
import { Link } from "react-router-dom";
import { signUp } from "../api/auth";
import { AuthContext } from "../App";
export const SignUp = () => {
const { setIsSignedIn, setCurrentUser } = useContext(AuthContext);
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [passwordConfirmation, setPasswordConfirmation] = useState("");
const confirmSuccessUrl = "http://localhost:3000";
const generateParams = () => {
const signUpParams = {
email: email,
password: password,
passwordConfirmation: passwordConfirmation,
confirmSuccessUrl: confirmSuccessUrl,
};
return signUpParams;
};
const handleSignUpSubmit = async (e) => {
e.preventDefault();
const params = generateParams();
try {
const res = await signUp(params);
console.log(res);
alert("confirm email");
} catch (e) {
console.log(e);
}
};
return (
<>
<h1>サインアップページです</h1>
<form>
<div>
<label htmlFor="email">メールアドレス</label>
<input
type="email"
id="email"
name="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div>
<label htmlFor="password">パスワード</label>
<input
type="password"
id="password"
name="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<div>
<label htmlFor="password_confirmation">パスワード確認</label>
<input
type="password"
id="password_confirmation"
name="password_confirmation"
value={passwordConfirmation}
onChange={(e) => setPasswordConfirmation(e.target.value)}
/>
</div>
<div>
<input
type="hidden"
id="confirm_success_url"
name="confirm_success_url"
value={confirmSuccessUrl}
/>
</div>
<button type="submit" onClick={(e) => handleSignUpSubmit(e)}>
Submit
</button>
</form>
<Link to="/signin">サインインへ</Link>
</>
);
};
7. サインインページ作成
-
サインアップした際に届くメール内のリンクをクリックしたらサインインページに遷移される。
-
このサインインページで、サインアップ時に登録したメールアドレスとパスワードを入力して Home コンポーネントに遷移したら成功。
-
サインアップコンポーネントで Submit ボタンをクリックして、メール内のリンクをクリックする前にサインインコンポーネントからログインしようとしても失敗する。
import Cookies from "js-cookie";
import { useContext, useState } from "react";
import { Link, useHistory } from "react-router-dom";
import { signIn } from "../api/auth";
import { AuthContext } from "../App";
export const SignIn = () => {
const { setIsSignedIn, setCurrentUser } = useContext(AuthContext);
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const history = useHistory();
const generateParams = () => {
const signInParams = {
email: email,
password: password,
};
return signInParams;
};
const handleSignInSubmit = async (e) => {
e.preventDefault();
const params = generateParams();
try {
const res = await signIn(params);
if (res.status === 200) {
Cookies.set("_access_token", res.headers["access-token"]);
Cookies.set("_client", res.headers["client"]);
Cookies.set("_uid", res.headers["uid"]);
setIsSignedIn(true);
setCurrentUser(res.data.data);
history.push("/");
}
} catch (e) {
console.log(e);
}
};
return (
<>
<p>サインインページです</p>
<form>
<div>
<label htmlFor="email">メールアドレス</label>
<input
type="email"
id="email"
name="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div>
<label htmlFor="password">パスワード</label>
<input
type="password"
id="password"
name="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<button type="submit" onClick={(e) => handleSignInSubmit(e)}>
Submit
</button>
</form>
<Link to="/signup">サインアップへ</Link>
</>
);
};
以上で Rails + React でのメール本認証のログイン機能が完成。
Discussion
まつしょー様
記事の内容を大変参考にさせて頂いております。
気になる部分がありましたので質問させて頂きます。
サインイン時(SignUp.jsx)の
handleSignUpSubmit
という関数内でCookiesへの値のセットとsetIsSignedIn(true);
とsetCurrentUser(res.data.data);
を行われておられますがメール認証を行う前のユーザーでログインもしていない状態で値をセットすることがこのコードで可能でしょうか。何度か試したのですが
Cookiesの値は全てundifined
、current_userの値はnil
となってしまいます。ご教授いただけると幸いです。
記事をご覧いただきありがとうございます。
そして、ご指摘ありがとうございます。
私も今確認させてもらったのですが、このメール認証の作りだと
setIsSigneIn(true)
とsetCurrentUser(res.data.data)
の値をセットすることはできませんメールで送ったリンクからログインページに遷移して、ログインをすることで値をセットすることができます。
以前作ったRailsAPIとReactでのログイン機能のコードを使いまわしていて確認不足でした!
私の完全な記述ミスなので申し訳ないです🙇
記述を修正したのでご確認ください!
ご丁寧にありがとうございました!