localStorageやsessionStorageでは実現できない、HttpOnly Cookieによる安全なJWT認証管理とその実装方法を徹底解説します。
📝 この記事を書くに至るまで
とあるプロジェクトで Rails と Next.js を組み合わせて開発していました。Rails 側では認証系で有名な devise-jwt
を使用し、Next.js 側からログイン・ログアウトなどの認証機能をつなぎ込んでいました。
まずぶつかった最初の壁は、JWTをどこに保管すべきか という問題でした。多くの記事を読み漁りましたが、どれも localStorage
やインメモリでの保管を推奨しているように見受けられました。
しかし、JSでアクセス可能なこれらの手法にはセキュリティ上のリスク(XSS)があるため、もっと安全な方法を探してたどり着いたのが、HttpOnly Cookie での管理でした。
ただしここで第2の壁に直面します。それは「HttpOnly Cookie を操作するにはサーバー側でのみ可能」という制約。Next.js のサーバーアクションやコンポーネント構成を深く理解していないと、実装が難航します。
本記事では、このプロセスで私がぶつかった壁と、それをどのようにして乗り越えたかを解説していきます。特に、App RouterとRailsの組み合わせでセキュアかつ快適にJWT認証を実装したい方の参考になれば幸いです。
✅ 本記事の対象読者
- RailsとNext.jsを組み合わせて認証機能を構築したい人
- JWTの保管方法に悩んでいる人
- HttpOnly CookieとServer Actionの使い方を知りたい人
- Next.js App Routerでの認証管理に困っている人
🔐 JWTはどこに保存すべき?
RailsのAPI側で devise-jwt
を使用する際、JWTの保存先が大きな検討ポイントになります。
❌ localStorage / sessionStorage の課題
- JavaScriptからアクセス可能 → XSSのリスク
- ページリロードで状態が消える
- 自動でHTTPリクエストに含まれない
✅ 解決策:HttpOnly Cookieの活用
HttpOnly
属性付きのCookieにJWTを格納することで、以下のようなセキュリティ上のメリットがあります。
- JavaScriptからアクセス不可
- HTTPリクエストに自動で付与される
- 状態が持続される
⚙️ Next.js × Railsでの認証構成
🍪 Cookie操作はServer Componentでのみ可能
Next.jsではCookieの読み書きは Server Action 内で行う必要があります。
🔁 useActionStateでClient→Server通信
useActionState
を使えば、クライアントからサーバー関数を安全に呼び出すことが可能です。
💡 実装例
// Client Component
'use client'
const [state, formAction] = useActionState(loginAction, initialState)
<form action={formAction}>
<input name="email" />
<input name="password" type="password" />
<button type="submit">ログイン</button>
</form>
// Server Action
'use server'
export async function loginAction(formData: FormData) {
const email = formData.get('email')
const password = formData.get('password')
const token = await loginAndGetJwt(email, password)
cookies().set('access_token', token, {
httpOnly: true,
path: '/',
secure: true,
sameSite: 'lax',
})
return { success: true }
}
// layout.tsx(Server Component)
const user = await getCurrentUser()
return <AuthProvider user={user}>{children}</AuthProvider>
🤔 よくある誤解:use clientの影響範囲
「use client を宣言すると子要素すべてがClient Componentになる」と誤解されがちですが、実際にはそのファイル内だけに影響します。
✅ OKな構成例
// layout.tsx(Server Component)
import AuthProvider from '@/providers/AuthProvider' // Client Component
import { getCurrentUser } from '@/lib/auth' // Server function
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const user = await getCurrentUser()
return (
<html lang="ja">
<body>
<AuthProvider user={user}>
{children} {/* ここは Server Component もOK */}
</AuthProvider>
</body>
</html>
)
}
✅ 実装構成まとめ(表形式)
項目 | 手法 |
---|---|
JWT保管 | HttpOnly Cookie によるサーバー側管理 |
Client→Server呼び出し | useActionState + server action |
ログイン状態の維持 | layout.tsx にて /me を取得し Provider に渡す |
コンポーネント分離 | Server Component / Client Component を明確に使い分け |
📌 まとめ
セキュアなJWT認証をNext.js(App Router)とRails(devise-jwt)で構築するには、HttpOnly Cookieの活用と、useActionState + server actionによる連携が鍵となります。
ぜひ、今回紹介したベストプラクティスを参考に、より安全かつ快適な認証設計を実現してください。