はじめに
現在、HappinessChainの課題でXクローンを作ってます。
そこでdevise-token-authを使って認証周りを作っていくのですが、
毎回作っていてやり方を忘れるので、前にも書いた気がしますが書き残そうと思ってやってます。
ちなみに上からやっていけばできる、みたいな感じではないです。
この記事の終わりになればできるようになってるという感じです。
前提
frontendは立ち上げたら next.jsの画面が見えること、
backendは立ち上げたら赤いRailsのロゴがある画面が見えることを前提に進めます。
なお、corsの設定等はまだしてません。
新規登録
Rails
必要に応じて下記を参照しながら進めます。
devise_token_auth/SUMMARY.md at master · lynndylanhurley/devise_token_auth · GitHub
gemを入れる
gem "rack-cors" gem "devise" gem "devise-token-auth"
deviseのinstall
rails g devise:install
色々指示が飛ぶのでapiモードで必要なものだけ
development.rbに下記を設定
config.action_mailer.default_url_options = { host: 'localhost', port: 8000 }
=> rails g devise:installをしないとtoken-authのinstallをした後の諸々が失敗します。
devise-token-authのinstall
devise_token_auth/docs/config/README.md at master · lynndylanhurley/devise_token_auth · GitHub
rails generate devise-token-auth:install User /api/v1 => 後に修正します。下の方がいいですね。 rails generate devise-token-auth:install User users
migrationファイルが作成されるので必要なものを入力してください。
今回はEメール認証をしたいのでconfirmableの部分はコメントアウト外してください。
その後に
rails db:migrate
corsの設定
devise_token_auth/docs/config/cors.md at master · lynndylanhurley/devise_token_auth · GitHub
# gemfile
gem 'rack-cors', :require => 'rack/cors'
# config/application.rb
module YourApp
class Application < Rails::Application
config.middleware.use Rack::Cors do
allow do
origins '*'
resource '*',
headers: :any,
expose: ['access-token', 'expiry', 'token-type', 'uid', 'client'], <= ここは必須
methods: [:get, :post, :options, :delete, :put]
end
end
end
end
exposeのところはこのように書いてねとあるのでそうします。
corsを設定する理由 => corsというのはcross origin resource sharingというもので異なるオリジン間でのリソース共有の制限と許可をする仕組みです。
オリジンというのはスキーム+ホスト名+ポート番号の組み合わせからなるものです。
例えばhttps:://localhost:443 というやつですね。
httpsがスキーム、ホスト名がlocalhost、ポート番号が443です。
知らないサーバからのアクセスを制限したり許可したりすることで自分を守るためのセキュリティ機能です。
Eメール認証を作る
コントローラを作るのと開発環境用にletter_openerを入れようと思います。
まずは新規登録です。
パスを設定する
/api/v1/usersというパスに指定のデータをpostしたらデータが投げられるようにしたいです。
ということでroute.rbにその情報を書いていきましょう。
現状、routes.rbは下記のようになっているかと思います。
# frozen_string_literal: true
Rails.application.routes.draw do
mount_devise_token_auth_for 'User', at: '/api/v1'
resources :tasks
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
# Defines the root path route ("/")
# root "articles#index"
end
この状態で rails routesをするとおかしいことに気づきます。
POST /api/v1(.:format) devise_token_auth/registrations#create
色々おかしいですね。これはなぜか。
devise_token_auth/docs/usage/routes.md at master · lynndylanhurley/devise_token_auth · GitHub
公式ドキュメントを見ます。
mount_devise_token_auth_for 'User', at: '/api/v1' としたら 'User'のところには認証対象であるモデルのクラス名を、
'/api/v1/'にはパスにprefixをつけたいときに使うようですね。
ちなみにどちらも未指定だとエラー吐かれるみたいですね。
atの方はhogeでも'/'渡しても機能するそうですね。
ただ / だとなんかわかりにくいし、hogeだとダサいし。
なので指定のprefixつけるみたいですね。
でだ。やりたいことは '/api/v1/users'に指定データをPOSTしたらUserがcreateされるようなパスを作る、です。
なので
mount_devise_token_auth_for 'User', at: '/api/v1/users' と書けばいいですね。
POST /api/v1/users(.:format) devise_token_auth/registrations#create
ただnamespace使うこともできますね。そっちの方が後々のことを考えたら良いかと思います。
# frozen_string_literal: true
Rails.application.routes.draw do
namespace :api do
namespace :v1 do
mount_devise_token_auth_for 'User', at: '/users'
end
end
resources :tasks
end
というかそもそも 最初から色々 パスが追加されていたけどそいつら何?と思った方は公式ドキュメントです。
devise_token_auth/docs/usage/README.md at master · lynndylanhurley/devise_token_auth · GitHub
ここに各パスがどういうものかどういう用途かが書いてあります。
ちなみにEメール認証は一番上のパスに飛ばせばいいらしいですね。必須情報も添えてあります。
あとで使いましょう。
登録用コントローラを作る
と言っても特にやることもないですね。
元々作られているコントローラに必須情報添えてpostすればuserが作られるので。
ということで試しに作ってみましょう。先ほどの公式ドキュメントの英文引用するとpathが / でmethodがPOSTのやつは
Email registration. Requires email, password, password_confirmation, and confirm_success_url params (this last one can be omitted if you have set config.default_confirm_success_url in config/initializers/devise_token_auth.rb). A verification email will be sent to the email address provided. Upon clicking the link in the confirmation email, the API will redirect to the URL specified in confirm_success_url. Accepted params can be customized using the devise_parameter_sanitizer system.
要は
{
"email": "hogehoge@mail.com",
"password": "hogehoge",
"password_confirmation": "hogehoge"
}
として今の場合なら"/api/v1/users"にPOSTすればいいみたいですね。
4つ目の"confirm_success_url"は "config/initializers/devise_token_auth.rb"に書いてなければ添える必要があるそうですが、
フロントでいちいち指定するのもかったるいのでrails側で設定しておきましょう。
次に確認メールを飛ばす必要があるらしいのでメールの設定をしていきましょう。
またconfirm_success_urlについては ログイン画面に飛ばせば良いと思うので http://localhost:3000/auth みたいにしときましょう。
さらに"config/initializers/devise_token_auth.rb"の末尾に”これ設定しないとメール飛ばさんよ"という記述があったので
これもtrueにしておきましょう。
config/initializers/devise_token_auth.rb 〜〜〜 # By default DeviseTokenAuth will not send confirmation email, even when including # devise confirmable module. If you want to use devise confirmable module and # send email, set it to true. (This is a setting for compatibility) config.send_confirmation_email = true <= こいつ config.default_confirm_success_url = 'http://localhost:3000/auth' end
あとは忘れていたapp/models/user.rbのconfirmableの設定を追記します。
# frozen_string_literal: true
class User < ActiveRecord::Base
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable,
:confirmable
include DeviseTokenAuth::Concerns::User
end
ここからはletter_openerを入れます。
gem letter_opener を試してみる #Rails - Qiita
はい。これを元に色々入れて、initilizersをいじったので再起動させていざ実践。
いけましたね。ログを見るといけてますね。
{
"email": "hoge@mail.com",
"password": "hogehoge",
"password_confirmation": "hogehoge"
}
を"http://localhost:8000/api/v1/users"にPOSTしたら

こんなメールが帰ってきて、このurlをクリックするとconfirm_success_urlのところに飛ぶみたいですね。
正確には"/api/v1/users/confirmation?~~~"にGETリクエストを送ってbackendで対象ユーザのconfirmed_at等がupdateされたあと
そこからリダイレクトされてconfirm_success_urlに飛ぶようです。
GET "/api/v1/users/confirmation?config=default&confirmation_token=[FILTERED]&redirect_url=http%3A%2F%2Flocalhost%3A3000%2Fauth"
ほー。あとはフロントの方作るだけですね。
これにてbackendの旅は終了です!
Next.js
Next.jsはpages routerで作ります

この画面を作りましょう。UIコンポーネントについてはshadcnを使っていきます。
使用するライブラリとしては axios、useSWR、react-hook-form、zod、react-toastifyです。
フロントについては作り方は特にないです笑
画面を作って、apiクライアント作ってpostすればいけると思います。
私はこんな感じで画面作りました!
export default function Auth() {
const {
isOpen: isOpenSignUp,
setIsOpen: setIsOpenSignUp,
open: openSignUp,
close: closeSignUp,
toggle: toggleSignUp,
} = useDisclosure();
return (
<>
<div className="flex flex-col h-screen">
<div className="flex-1 flex">
{/* ロゴ */}
<div className="flex-1 flex justify-center items-center">
<Image src="/logo-white.png" alt="logo" width={300} height={300} />
</div>
{/* 入力フォーム */}
<div className="flex-1 p-4">
<div className="flex flex-col p-4 h-full">
<div className="my-10 text-6xl font-bold">
すべての話題が、ここに。
</div>
<div className="text-4xl mb-5 font-bold">
今すぐ参加しましょう
</div>
<div className="grow flex-1 flex flex-col gap-2 justify-between">
<div className="flex flex-col gap-2">
<Button
variant="secondary"
className="w-[300px] rounded-full font-bold"
>
<Radius className="w-4 h-4" />
Google
</Button>
<Button
variant="secondary"
className="w-[300px] rounded-full font-bold"
>
<Apple className="w-4 h-4" />
Apple
</Button>
<Separator className="w-[300px] my-5 flex justify-center border-gray-500">
または
</Separator>
<Button
className="w-[300px] rounded-full bg-sky-500 font-bold hover:bg-sky-600"
onClick={openSignUp}
>
メールアドレスで登録
</Button>
<p className="text-sm text-gray-500 mb-3 w-[300px]">
アカウントを登録することにより、利用規約とプライバシーポリシー(Cookieの使用を含む)に同意したとみなされます。
</p>
</div>
<div className="flex flex-col mt-5">
<p className="text-sm text-gray-500">
アカウントをお持ちの方はこちら
</p>
<Button
variant="secondary"
className="w-[300px] rounded-full text-sky-500 font-bold"
onClick={openSignIn}
>
ログイン
</Button>
</div>
</div>
</div>
</div>
</div>
{/* フッター */}
<div className="">
<div className="font-sm text-gray-500">
基本情報 Xアプリをダウンロード ヘルプセンター 利用規約
プライバシーポリシー Cookieのポリシー アクセシビリティ 広告情報
ブログ 採用情報 ブランドリソース 広告 マーケティング Xのビジネス活用
開発者 プロフィール一覧 設定 © 2025 X Corp.
</div>
</div>
</div>
{/* ダイアログ */}
<SignUpDialog
isOpen={isOpenSignUp}
setIsOpen={setIsOpenSignUp}
onClose={closeSignUp}
/>
</>
);
}
type Props = {
isOpen: boolean;
setIsOpen: (isOpen: boolean) => void;
onClose: () => void;
};
export const SignUpDialog = ({ isOpen, setIsOpen, onClose }: Props) => {
const form = useForm<SignUpSchemaType>({
resolver: zodResolver(SignUpSchema),
defaultValues: {
email: "",
password: "",
password_confirmation: "",
},
});
const onSubmit: SubmitHandler<SignUpSchemaType> = async (data) => {
try {
const response = await signUp(data);
toast.success("認証メールを送信しました");
console.log(response);
} catch (error) {
console.log(error);
}
};
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogContent className="p-1 h-[80vh] overflow-y-auto">
<div className="flex flex-col gap-2">
{/* header */}
<div className="flex justify-between items-center">
<Button
size="icon"
variant="ghost"
className="rounded-full p-1 "
onClick={onClose}
>
<X className="w-10 h-10 text-foreground" />
</Button>
<div className="flex-1 flex justify-center">
<Image src="/logo-black.png" alt="logo" width={30} height={30} />
</div>
<div></div>
</div>
{/* body */}
<div className="grow flex flex-col px-10">
<form
onSubmit={form.handleSubmit(onSubmit)}
className="grow flex flex-col gap-5 text-foreground"
>
<div className="text-3xl font-bold">アカウントを作成</div>
<div className="grow flex flex-col justify-between gap-4">
<div className="flex flex-col gap-8">
<div className="items-center gap-4">
<Label htmlFor="email" className="">
メールアドレス
</Label>
<Input
id="email"
{...form.register("email")}
className=""
placeholder="example@gmail.com"
/>
{form.formState.errors.email && (
<p className="text-red-500">
{form.formState.errors.email?.message as string}
</p>
)}
</div>
<div className="items-center gap-4">
<Label htmlFor="password" className="">
パスワード
</Label>
<PasswordInput form={form} name="password" />
</div>
<div className="items-center gap-4">
<Label htmlFor="password_confirmation" className="">
確認用パスワード
</Label>
<PasswordInput form={form} name="password_confirmation" />
</div>
<div className="text-sm text-gray-500">
アカウントを登録することにより、利用規約とプライバシーポリシー(Cookieの使用を含む)に同意したとみなされます。
</div>
</div>
<div className="flex justify-center items-center mb-8">
<Button
className="w-[300px] rounded-full bg-sky-500 font-bold hover:bg-sky-600"
onClick={() => form.handleSubmit(onSubmit)}
>
アカウントを作成
</Button>
</div>
</div>
</form>
</div>
</div>
</DialogContent>
</Dialog>
);
};
ご参考までに!
ログイン
Rails
続いてログイン機能です。
devise_token_auth/docs/usage/README.md at master · lynndylanhurley/devise_token_auth · GitHub
公式ドキュメントを見ると下記の記載がありました。
/sign_in POST Email authentication. Requires email and password as params. This route will return a JSON representation of the User model on successful login along with the access-token and client in the header of the response.
/sign_inというパス、今回ならhttp://localhost:8000/api/v1/users/sign_inというパスに emailとpasswordをpostすれば良いそうです
でレスポンスヘッダにaccess-tokenとclientというものが添えられて帰ってくるらしいですね
やってみましょう。
ということで先ほど 登録した後の認証メールに記載されたurlをクリックしたuserでログインを行います。

ログインできました。
下側のレスポンスヘッダの部分を見てみると access-tokenとclient、expiry, uid欄に何やら値が追加されています。
毎リクエストごとにこれらを使う必要があるそうです。
では次にauthenticate_api_v1_user!とかいう特有のやつやったらいけるかの確認です。
下記のコントローラを作ります。
# frozen_string_literal: true
class Api::V1::HealthCheckController < ApplicationController
before_action :authenticate_api_v1_user!
def index
render json: { message: 'OK', user: current_api_v1_user }, status: :ok
end
end
route.rbはこんな
# frozen_string_literal: true
Rails.application.routes.draw do
mount LetterOpenerWeb::Engine, at: '/letter_opener' if Rails.env.development?
namespace :api do
namespace :v1 do
mount_devise_token_auth_for 'User', at: '/users'
get 'health_check', to: 'health_check#index'
end
end
resources :tasks
end
でやってみるとなんかエラーが出ました。
と。なんかエラーです。
ActionDispatch::Request::Session::DisabledSessionError (Your application has sessions disabled. To write to the session you must first configure a session store):
これはあれです。私もハマりました。
これは devise、元であるwardenはsessionを使う前提で動作しているが RailsのAPIモードではセッションを扱う機能が除外されているからです。
そのため追加で設定する必要があります。
application.rbにこれを
config.middleware.use ActionDispatch::Cookies
config.middleware.use ActionDispatch::Session::CookieStore
application_controller.rbにこれを
include ActionController::Cookies
追加してください。
また巷ではrack_session.rbとかいうものを作って擬似的にセッションを作って回避する?みたいなやり方もあるそうですが
こっちの方がRailsぽいし、なにぶん書いていて理解できるのでこっちにしましょう。
ということでリトライ。
行けました。
ということでbackendについては3行追加しただけですね。楽です。
フロント
ということですがこちらも作り方は特にありません。
modalを1つ作ればいいだけですね。
sign_up_modalをパクってください笑
少し変えればいけます。
終わりに
ということでおそらくdevise-token-authを使って認証をする際、最も詰まるのはbackendの設定かと思います。
ただ公式ドキュメント読んでたらいけます。
ちなみに私はこの道を3回通りました。
最初の1回が Rails x React(JavaScript)、2回目が Rails x Next.js(TypeScript)、そして3回目がこれです。
2回目までは今回と変わらない要件なのに deviseのコントローラ作っていたり、confirmation_contrllerとか作って
サーバの方でリダイレクトさせてました。まともに読めてなかったです、公式の方。
で読んだ結果、もう大丈夫になりました。基本のきのところですが。
昔より、「なぜこうする必要があるのか」ということが少しわかったような気がします。
ということで頑張っていきましょう。