Nuxt × AWS Amplify(Cognito) で SSR 認証を成立させるための実践アーキテクチャ

前回の記事では、Nuxt × Amplify 認証の仕組みを整理しつつ、最後に「AWS Amplify と HttpOnly Cookie 対応について」という補足を書きました。

🔗 前回の記事

magnet88jp.hateblo.jp

今回は、その補足として触れた Cookie ベースのサーバーセッション方式」 を実際に Nuxt へ実装した話をまとめます。


AWS Amplify(Cognito User Pool)を使った認証はとても便利ですが、実のところ SSR(サーバーサイドレンダリング)と相性が悪い という制約があります。

  • Amplify の認証は SPA(CSR)前提 に設計されている
  • 認証情報(idToken / accessToken)は ブラウザの JavaScript からしか取得できない
  • SSR 中は fetchAuthSession() が実行できない
  • 結果、SSR ページは「ログイン状態」を判定できない
  • server/api から Amplify Data や Backend を呼ぶ場合も token が取れず認証が崩れがち

❗なぜ Amplify は SSR と相性が悪いのか?

Amplify のログイン状態は以下のように管理されています:

ログイン → fetchAuthSession() → idToken, accessToken を JS で保持

しかし SSR では JS が動作しないため、

  • idToken の値がない
  • トークンを使う API 認証が成立しない
  • SSR で状態の判定ができない

という根本的問題が起きます。

つまり SSR の世界で「認証情報が存在しない」 のです。

この問題を解決しない限り、Nuxt の SSR はまともに動きません。


🔑 そこで必要なのが「サーバー側セッション」です

Nuxt は SSR 中に Cookie のみを自動で送信できます。

この性質を活かし、認証方式を以下のように再設計します:

1. クライアントで Cognito にログイン(CSR)
2. idToken をサーバーの API に送る
3. サーバーで検証 → 署名済みの JWT に作り直し、HttpOnly Cookie として保存
4. SSR / server API は Cookie を読むだけで認証可能

ポイントは idToken をそのまま Cookie に入れない こと。

攻撃面を減らすため、

  • サーバー側で署名した独自 JWT
  • HttpOnly + secure + sameSite=Lax
  • 有効期限は Cognito の exp に合わせる

というセキュアな形へ変換します。

これにより、

  • SSR
  • server/api
  • Amplify Data の backend 呼び出し

すべてが自然にログイン状態を扱えるようになります。


🏗️ 実際のフロー(全体図)

[CSR] Amplify Auth でログイン
         │
         ▼
 fetchAuthSession() で idToken を取得
         │
         ▼
   POST /api/auth/session
    (idToken を送る)
         │
         ▼
[Server] Cognito 公開鍵で idToken を検証
         │
         ▼
   user情報 + idToken を含む JWT を生成
         │
         ▼
 HttpOnly Cookie 'app_session' として保存
         │
         └───────────┐
                     ▼
      SSR / server/api / Middleware
       Cookie を参照 → 認証が成立

🧩 セッション Cookie の構造

サーバー側が発行する Cookie(app_session)は次のような情報を含みます:

  • sub(ユーザーID)
  • email
  • cognito:groups
  • idToken(Amplify Data の認可に使用)
  • exp(Cognito の期限に合わせる)

これらを jsonwebtoken で署名し、HttpOnly Cookie として保存します。


🎯 この方式のメリット

SSR と認証が自然に動く

SSR 中の API 呼び出しやページ描画で、常にログイン状態を復元できます。

idToken を JavaScript で保持する必要がないため、攻撃面が大きく減ります。

✔ Amplify 特有の制約を回避

Amplify の “CSR でしか動かない問題” をバイパスし、SSR 時代の Amplify を可能にするアーキテクチャになります。

✔ server/api でも Amplify Data が実行できる

Amplify Data は authToken が必要ですが、 idToken を cookie から安全に取り出すことで SSR でも使えます。


🆚 Bearer トークン vs HttpOnly Cookie

Nuxt + Amplify では、Cookie 方式が圧倒的に合理的です。

観点 Bearer トーク HttpOnly Cookie
XSS 耐性 ❌ 弱い ◎ 強い
SSR での利用 ❌ ほぼ不可 ◎ 自然に動く
server/api トークン必須で煩雑 Cookie だけで認証
クロスドメイン ◎ 強い △ 設計が必要
管理の复杂性 高い 低い

🔨 実装ポイントまとめ

1. CSR:Amplify でログイン

const session = await fetchAuthSession()
const idToken = session.tokens.idToken.toString()

2. idToken をサーバーへ送信

await fetch('/api/auth/session', {
  method: 'POST',
  credentials: 'include',
  body: JSON.stringify({ idToken }),
})
  • verifyIdToken で正当性を確認
  • 必要な user 情報を抽出
  • JWT に署名して cookie に保存
  • HttpOnly / secure / sameSite=Lax を徹底
event.context.user = {
  sub,
  email,
  groups,
  idToken,
}

🧠 Nuxt × Amplify で SSR を使う人の“新しい定石”へ

今回紹介した方式は、実は Amplify Auth を SSR で扱うための最適解にかなり近い と考えています。

Amplify の制約(CSR 前提)を踏まえながら:

  • セキュアで
  • SSR と相性がよく
  • server/api や Amplify Data の backend 呼び出しにも対応し
  • 拡張性の高い

現実的で実装しやすい認証アーキテクチャです。

Nuxt で本格的に SSR と認証を両立させたい人には、ぜひ試してみてほしいやり方です。

Nuxt4 + Amplify(Cognito)で起きた「Missing bearer token」問題と SSR 認証のリアル

この記事では、Nuxt4 と AWS Amplify(Cognito 認証)を組み合わせた掲示アプリ開発の中で起きた、 「ユーザー一覧ページが初期表示で Missing bearer token エラーになる」 という問題と、その修正内容・背景技術について紹介します。

同じ構成でアプリを作っている方や、Nuxt + Cognito の SSR まわりでハマる方の参考になれば幸いです。


🔧 プルリクの概要

問題が発生したのは 管理者向け「ユーザー一覧ページ」です。

ページの初期表示時に、内部で /api/users を叩いてユーザー一覧を取得します。しかし SSR(サーバーサイドレンダリング)により、以下のコードがサーバー側で実行されました。

await useAsyncData('admin-users-list', () => $fetch('/api/users'))

ところが Cognito 認証が必要な /api/users では Authorization ヘッダーに Bearer Token が必要

しかし SSR のタイミングでは Cognito のトークンがまだ存在せず、結果として:

Missing bearer token

というエラーが発生。

このプルリクは、初期表示時に Missing bearer token が出る問題を修正するプルリクです。


🎯 修正内容:SSR をやめて "クライアント側で" 認証後に fetch する方式に変更

最終的には、API の呼び出しタイミングを SSR → クライアント側へ移動することで解決しました。

修正後の流れは次のとおり:

onMounted(async () => {
  start()      // 認証情報の準備
  await sync() // LocalStorage の Cognito トークンを同期
  await loadUsers() // 認証ヘッダー付きで /api/users を fetch
})

要点は以下。

  • SSR(サーバー側)では Cognito トークンを取得できない
  • sync() によりクライアント側でトークンが揃う
  • buildHeaders() が正しい Authorization ヘッダを生成
  • $fetch('/api/users') が正常に実行されるようになる

結果、Missing bearer token は完全に解消されました。


🔍 なぜ SSR で Missing bearer token が出たのか?

✔ Cognito は LocalStorage 管理の SPA 認証方式

Amplify(Cognito)の認証情報はこう管理されています:

  • accessToken → LocalStorage
  • idToken → LocalStorage
  • refreshToken → LocalStorage

👉 SSR(サーバー側)では LocalStorage が存在しないため、トークンを取得できない


SSR の useAsyncData() は “サーバー側で” 一度実行される

そのため…

  • まだ認証同期が済んでいない
  • LocalStorage にアクセスできない
  • 認証ヘッダーに必要な Bearer Token を組み立てられない

という状況になります。

つまり構造的に、

Cognito の認証トークンは SSR では読み取れない

という問題が根本原因です。


🤔 そもそも、Cognito で SSR 認証を行わないのは一般的なのか?

結論:はい、一般的です。むしろ Cognito は SSR 認証が苦手なサービスです。

理由は明確で、

  • Cognito 認証は SPA / モバイル向け
  • 認証情報はブラウザに保存される(LocalStorage)
  • SSR(サーバー)からセッションを参照できない
  • HttpOnly Cookie による SSR 認証方式を前提としていない

この構造は、SPA 向けには最高に便利ですが、 SSR アプリケーションには向かない側面があります。


📈 一方で、SSR 認証が必要になるユースケースもある

たとえば:

① URLを直接開いたときも SSR で認証を反映したい管理画面

初期描画からユーザー一覧やダッシュボードを表示したい場合。

SEO が必要なページ+ログイン状態による表示差分

ブログの下書きプレビューなど。

SSR でサーバー側 API にユーザー認証情報を渡したいケース

SSRAPI → DB」のようにサーバー中心で処理する場合。

こういった “SSR の時点でログイン状態を使いたい” ユースケースでは、 Cognito の LocalStorage トークン方式では限界があるのです。


🔥 Auth0 を使うとどうなる? → SSR 認証が実現できる

Auth0 には HttpOnly Cookie 方式の SSR 認証が組めます。

具体的には:

  1. Universal Login でログイン
  2. サーバー側(Nuxt)でコード交換
  3. access_token を HttpOnly Cookie に保存
  4. SSR 時に Cookie を読み取り、認証情報を復元
  5. SSR の useAsyncData() でも認証済み fetch が可能

つまり、今回の PR のような「SSR で Missing bearer token」が Auth0 なら根本的に発生しない構造が作れます。


📝 まとめ:Nuxt × Cognito の SSR は構造的に難しい

今回発生した「Missing bearer token エラー」は、 Cognito が SSR 認証に向かないことによる構造的な問題でした。


✅ 課題

SSRuseAsyncData() がサーバー側で実行される ↓ サーバー側では Cognito トークンがない ↓ Authorization ヘッダを生成できない ↓ Missing bearer token が発生


✅ 解決

クライアント側 onMounted() で認証同期後に fetch → 認証ヘッダーも正しく付与され正常に動作


✅ 学び

  • Cognito の LocalStorage トークン方式は SSR と相性が悪い
  • SSR 認証が必要なアプリなら、Cookie セッション型(Auth0など)が向く
  • Nuxt / Amplify 構成では「認証 APICSR で呼ぶ」が王道

🎉 おわりに

今回の修正は一見小さな変更ですが、背景には Cognito の認証方式と SSR の本質的な相性問題 という深い技術的テーマがありました。

Nuxt + Amplify で SSR を使う方や、Cognito の制約に悩む方の参考になれば幸いです。

以下は、URL を自然に含めた「ブログの最後に添えるパッチ文章」です。内容を壊さず、一般読者にも読みやすい形でまとめています。


今回の調査の中で、AWS Amplify(Authentication モジュール)が HttpOnly Cookie をサポートし始めているという公式アナウンスも確認しました。

特に、2025年3月に公開された以下の発表では、 Next.js などのサーバーサイドレンダリングSSR)アプリ向けに、HttpOnly Cookie を用いたセキュアな認証管理がサポートされたと明記されています。

🔗 参考: https://aws.amazon.com/jp/about-aws/whats-new/2025/03/aws-amplify-httponly-cookies-server-rendered-next-js-applications/

Cognito 自体が直接「HttpOnly Cookie に ID トークン/Access トークンを保存する」仕組みを提供しているわけではありませんが、 Amplify 側で Cookie ベースのセッション管理を実現するレイヤーを提供し始めたという点は非常に興味深い動きです。

この部分はまだ十分に検証できていないため、 Amplify がどのように Cookie を生成し、署名/暗号化し、検証するのか Nuxt / Nitro で同様の構成が再現できるのか といったポイントを引き続き調査し、別記事として整理する予定です。

Amplify Gen2(Data v6)で `owner` が自動で入らない理由と解決までの全記録

― allow.owner() の正しい理解と、ユーザープール認証が必須になる理由

Amplify Gen2(Amplify Data v6)で owner フィールドを使った認可(Authorization)を設定したにもかかわらず、

  • 新しく作成したレコードに owner が自動で入らない
  • DynamoDB のテーブルにも owner カラムが出てこない

という現象に遭遇しました。

本記事では、その原因の追跡過程、Amplify Data v6 の仕様、そして最終的に問題を解決するために必要だった設定をまとめます。


🎯 結論(先にまとめ)

Amplify Data v6 における allow.owner()User Pool(Cognito)認証で create したときだけ owner を自動セットする

ところが今回の BoardPost は create が authenticated ルールのみで許可されていたため、owner ロジックが呼ばれず、レコードに owner が入らなかった。


1. そもそも Amplify Data v6 の allow.owner() の仕様は?

Amplify Gen2 の公式ドキュメントより:

  • allow.owner()モデルの owner フィールドを使って「所有者だけに CRUD を許可する」ルール
  • owner フィールドは明示的に書くことも、書かなくてもよい(自動追加される)
  • User Pool 認証(authMode: 'userPool')で create したときだけトークン内の identity(通常は sub)が owner に自動セットされる

つまり、

authMode が userPool でない場合、owner は自動で入らない

ということが仕様として確定しています。


2. レコード作成時の authMode は正しく userPool だった

調査のためにサーバー側でログを追加したところ、次のように表示されていました:

[BoardPosts] authMode userPool hasToken true

つまり、

  • 認証は 正しく Cognito User Pool
  • 有効な JWT(authToken)も渡されている

→ それでも owner が入らない。

この時点で「authMode の問題」ではないことが確定しました。


3. 本当の原因:allow.owner().to([...]) に create が含まれていなかった

BoardPost の auth 設定はこうなっていました:

.authorization(allow => [
  allow.guest().to(['get', 'list', 'listen', 'sync']),
  allow.authenticated().to(['get', 'list', 'listen', 'sync', 'create']),
  allow.owner().to(['get', 'update', 'delete']),
])

注目すべきポイント:

  • create は allow.authenticated() の方で許可している
  • owner ルールの中には create が含まれていない

Amplify の owner 自動設定は、

「owner ルールで create が許可されている」 かつ 「authMode が userPool」

という 2 条件を満たしたときにだけ動く仕組みになっています。

今回の設定は create が owner ルールに含まれていないため、

create 実行時の owner 自動付与ロジックが発火しない

という状況が生まれていました。


4. DynamoDB に owner カラムが表示されなかった理由

DynamoDB はスキーマレスなので、1 件も owner を持つレコードが無ければ列として表示されません。

owner が入らない状態のまま create を何度行っても、

  • owner を持つアイテムが 1 つも存在しない
  • → DynamoDB コンソールに owner 列が表示されない

という挙動になります。

これは仕様通りです。


5. 解決方法:owner に create を含める

以下のように、

  • owner 側に create を含める
  • もしくは allow.owner() のショートカットを使う

ことで owner 自動セットが動きました。

(A)ショートカットで全操作を owner に紐付ける

allow.owner()

= create / read / update / delete を包括する

(B)明示的に create を含める方法

allow.owner().to(['create', 'read', 'update', 'delete'])

6. 修正後:owner が正しく入るようになった

修正後に投稿を作成すると、DynamoDB に以下のように owner が保存されました。

スクリーンショット

owner-success


7. まとめ

今回の問題をまとめると次の 3 点に集約されます。

✅ 1. allow.owner() の owner 自動セットは create 時にだけ動く

✅ 2. create が owner ルールに含まれていないと owner はセットされない

✅ 3. DynamoDB に owner 列が表示されないのは「owner を持つレコードが1件もない」から

Amplify Data v6 はシンプルになったといわれていますが、 このように 認可ルールの書き方によって自動フィールド挙動が変わる ため、 挙動が分かりにくくなるケースがあります。


8. おまけ:おすすめの認可設定テンプレート

掲示板用途であれば、以下の設定が分かりやすく安全です。

.authorization(allow => [
  allow.guest().to(['read']),
  allow.authenticated().to(['read', 'create']),
  allow.owner(), // ← 所有者は CRUD すべて
])
  • 投稿はログインユーザーなら作成可能
  • 閲覧はゲストも OK
  • 更新・削除は owner のみ

おわりに

Amplify Gen2(Data v6)での owner 自動セット周りは、 意図しない挙動になりやすいため、本記事が同様のトラブルで悩むユーザーの助けになれば嬉しいです。

他にも Amplify Data v6 / AppSync resolver の挙動で気になる点があれば、気軽にコメントください。

Amplify Gen2(v6)でのデータ認証ルールと `observeQuery` の正しい使い方

― 未認証ユーザー・認証ユーザー(オーナー/非オーナー)で“見えるデータ”を正しく設計する方法

Amplify Gen2(v6)の Data モデル認証はとても強力ですが、初見では挙動が直感的ではありません。

特に、掲示板アプリなど「未認証でも閲覧可能」「認証済みなら投稿可能」という一般的な要件で、

  • 認証後に逆に一覧が見えなくなる
  • observeQuery の結果が count: 0 になる
  • guest では見えるのに authenticated では見えない
  • owner をつけたらエラーは消えたが理由がわからない

といった現象にハマりやすく、多くの開発者が混乱します。

本記事では、公式ドキュメントの内容と実際のGen2の挙動を踏まえ、

  • 認証ルールの正しい書き方
  • observeQuery と authMode の関係
  • owner ルールの落とし穴
  • 未認証/認証ユーザーで何が “見える” のか

体系的に理解できる実践ガイド としてまとめました。


1. Amplify Gen2 認証モデルの基本思想

■ すべては “deny by default”

Amplify Gen2 の Data は 許可された操作以外すべて拒否 します。

allow.guest().to(['get', 'list'])

と書けば guest の get/list は OK、 それ以外は 全部 NG

■ ルールは OR 条件で評価される

allow.guest()
allow.authenticated()
allow.owner()

どれか1つでも通れば認可される仕組みです。

■ owner を使うなら owner: a.string() が必須

モデルに owner フィールドがなければ owner 認証は機能しません。


2. なぜ認証すると「見えなくなる」現象が起きるのか?

次のケースが典型的です。

allow.guest().to(['get', 'list'])
allow.owner()

一見正しく見えますが…

✔ 未認証(identityPool)

  • guest → 適用される → 一覧が全件見える

✔ 認証済み(userPool)

  • guest → 適用されない!!
  • owner → 自分の投稿だけ
  • authenticated → 未定義

他人の投稿がすべて消える

これが「認証したら見えなくなる」原因です。

Amplify Gen2 は「認証済み=guestルールを継承する」仕組みではありません。


3. 認証済みユーザーも “公開データ” を読めるようにするには?

結論:authenticated にも guest と同等の read 権限を付与する

allow.guest().to(['get', 'list', 'listen', 'sync'])
allow.authenticated().to(['get', 'list', 'listen', 'sync'])
allow.owner().to(['update', 'delete'])

これが 最も安全で混乱がない 書き方です。


4. observeQuery と authMode の関係を理解する

observeQuery 呼び出し例

client.models.BoardPost.observeQuery({ authMode })

ここでの authMode は AppSync へ どの認証方法でアクセスするか を指定しています。

authMode 実際の認証 想定ケース
userPool Cognito User Pool トーク 認証済みユーザー
identityPool Cognito Identity Pool (未認証扱い) 未認証ユーザー
apiKey パブリック API Key 公開API
iam IAM (SigV4) サーバー用途

authMode を間違えると何が起きる?

認証済み + authMode = userPool

→ authenticated / owner のルールのみ評価 → guest のルールは 完全に無視

認証済み + authMode = identityPool

→ guest のルールが評価される(未認証扱い) → 一覧が見える(ただし create 不可)


5. 正しい authMode 戦略

掲示板のように「未認証=閲覧OK」「認証済み=閲覧+投稿OK」の場合、

状態 authMode 期待する振る舞い
未認証 identityPool guest の読み取りルールで閲覧可
認証済み userPool authenticated で読み取り+create 可

※ guest ルールと authenticated ルールを明示しておかないと 認証後に読み取りが消えるので注意。


6. 掲示板アプリの正しいモデル定義(完全版)

BoardPost: a.model({
  author: a.string(),
  message: a.string().required(),
  imageKeys: a.string().array(),
})
.authorization(allow => [
  // 未認証ユーザーでも一覧取得 OK
  allow.guest().to(['read']),

  // 認証済みユーザーも同等に閲覧
  allow.authenticated().to(['read']),

  // 自分の投稿のみ作成・更新・削除可能
  allow.owner().to(['get', 'create', 'update', 'delete']),
])

これだけで、

  • 未認証:閲覧OK(投稿不可)
  • 認証済み:閲覧OK
  • 投稿・編集・削除:owner のみ

という自然な挙動になります。


🔧【修正】7. owner フィールドがないと起きる“悲劇”

※この記事公開後の追加検証により、内容の一部に誤りがあることが分かったため、 2025-11-27 に本節を修正しました。

owner ルールを使う場合、以下が必須です。

~~owner: a.string()~~

さらに create 時に owner を入れないと…

  • update / delete が全拒否
  • observeQuery が owner 不一致で非表示
  • Unauthorized(401/403)が発生
  • リアルタイム購読が落ちる

Amplify は create 時に自動で owner を設定しません。
(AppSync の v1 の時とは異なります)

必ず自前で owner を渡す必要があります

❌【旧結論】owner フィールドがないと owner 認証は動かない

(これは誤りでした)

✅【新結論】Amplify Gen2(v6)の allow.owner()

authMode が userPool のとき、owner フィールドを明示しなくても自動で付与・セットされる

今回の調査で判明したのは、

owner が自動で入らなかった理由は 「owner フィールドを書かなかった」からではなく 「authMode が userPool になっていなかった」から

という点です。


✔ なぜ owner フィールドを明示しなくても動くのか?

Amplify Data v6 では、

  • allow.owner() を使う
  • create が userPool の JWT で実行される

この 2 条件が揃った場合、AppSync resolver が

owner: "<Cognitoユーザーのsub:username>"

を自動で追加します。

つまり、以下のような定義でも owner は自動で付きます:

BoardPost: a.model({
  author: a.string(),
  message: a.string(),
})
.authorization(allow => [
  allow.owner(), // owner フィールドは自動追加される
])

✔ 今回 owner が入らなかった本当の原因

調査の結果、owner が空だった理由は次の 2 点でした:

① create の authMode が userPool ではなかった

identityPool のまま create が実行されていたため、 Amplify は「ユーザーの identity が確定しない」と判断して owner を付加しません。

② owner ルールに create が含まれていなかった

allow.owner().to(['update', 'delete']) だと、 create 時に owner 自動セット処理が発火しないため、owner が空のまま残ってしまう。


✔ 修正後(authMode=userPool)では owner が自動セットされるようになった

authMode と認可ルールを正しく直したあとは、 DynamoDB に以下のように owner が問題なく入るようになりました。

"owner": "cognitoUserSub:username"

✔ 修正まとめ(この記事への変更点)

【変更前の理解】

  • owner フィールドが無ければ owner 認証は動かない
  • owner 自動セットはされないため owner: a.string() は必須

【変更後の正しい理解】

  • owner 自動セットは authMode=userPool で create した場合にのみ動作
  • owner: a.string() は必須ではない(あっても良いが、無くても動く)
  • owner が空だった原因は認証モードの問題だった
  • allow.owner().to(['create']) の指定も重要 (create が owner ルールに含まれないと owner 自動付与ロジックが動かない)

✔ この記事を読んでいる開発者の方へ(注意点)

Amplify Gen2 の owner は、

authMode が userPool で create が owner ルールに一致するときのみ自動セットされる

という振る舞いであり、 「owner フィールドの有無」ではなく「認証方式」の影響が極めて大きい点に注意が必要です。


8. 最終まとめ:Amplify Gen2 の認証は「guest と authenticated を明示せよ」

Amplify Gen2 はデフォルトが “すべて拒否” のため、

  • guest には guest の権限
  • authenticated には authenticated の権限
  • owner には owner の権限

それぞれ明示的に書くことが重要 です。

特に observeQuery は authMode とルールの食い違いに敏感で、 設定がズレると簡単に「0件」になってしまいます。

✔ 正しいモデル定義にすることで…

  • 未認証でも一覧閲覧OK
  • 認証済みでも全投稿閲覧OK
  • 自分の投稿のみ編集OK
  • observeQuery も安定して動作

という理想的な掲示板アプリが構築できます。

Amplify Gen2 × Nuxt4 の observeQuery が Unauthorized になる理由と解決方法

〜 リアルタイム更新に失敗する問題の真因は「owner フィールドが無い」〜

Nuxt4 + Amplify Gen2(Data / GraphQL)でリアルタイム購読を行うために
client.models.XXX.observeQuery() を使っていたところ、

SUBSCRIPTION ERROR:
{
  errorType: 'Unauthorized',
  message: 'Not Authorized to access listBoardPosts on type Query'
}

というエラーが、ログイン(userPool)状態のときにのみ発生するという問題に悩まされました。

最終的に分かった結論は 「owner フィールドを model に定義していなかった」ことが原因 でした。

このブログでは、同じ問題でハマっている人向けに、再現条件と解決方法をわかりやすくまとめます。


✨ 結論:owner 認可を使うなら、owner フィールドが必須

Amplify Gen2 の GraphQL Auth ルールで allow.owner() を書いた場合、
そのモデルには 必ず owner を判別するフィールドが必要 です。

例:

allow.owner().to(['get', 'list', 'update', 'delete'])

と書くと、

  • 「このレコードの owner は誰か?」
  • 「今の Cognito ユーザーは owner と一致するのか?」

を判定する必要があります。

しかし、モデル側が次のように owner プロパティを持っていない状態だと……?

BoardPost: a.model({
  author: a.string(),
  message: a.string(),
  imageKeys: a.string().array(),
})
.authorization(allow => [
  allow.owner(),   // ← owner フィールドが無いのに owner 認可を使用
])

❌ 誰が owner なのか判別できないため、Amplify が自動判定に失敗し Unauthorized を返す

特に observeQuery で購読を開始すると、

  • onCreate
  • onUpdateここで Unauthorized が発生
  • onDelete

などのイベントが走りますが、
owner かどうか分からないため 更新イベント(onUpdate)が弾かれる という形で問題が表面化します。


🧪 実際に起きていた症状

● ゲスト(identityPool)のとき

→ 問題なし(そもそも owner 判定しないので)

● ログイン(userPool)のとき

SUBSCRIPTION ERROR: Unauthorized が発生
→ 初回ロード時に「リアルタイム更新に失敗しました」と表示
→ list は取得できるが、購読だけ失敗する

エラー内容:

Not Authorized to access listBoardPosts on type Query

✅ 解決方法:owner フィールドを追加するだけ!

モデル定義に owner を追加します。

Before(エラーになる):

BoardPost: a.model({
  author: a.string(),
  message: a.string(),
  imageKeys: a.string().array(),
})
.authorization(allow => [
  allow.guest().to(['get', 'list']),
  allow.authenticated().to(['get', 'list', 'create']),
  allow.owner(),   // ← これを使うなら owner フィールドが必要
])

After(エラー解消):

BoardPost: a.model({
  owner: a.string(),              // ← これを追加!
  author: a.string(),
  message: a.string().required(),
  imageKeys: a.string().array(),
})
.authorization(allow => [
  allow.guest().to(['get', 'list', 'listen', 'sync']),
  allow.authenticated().to(['get', 'list', 'listen', 'sync', 'create']),
  allow.owner().to(['get', 'list', 'listen', 'sync', 'update', 'delete']),
])

これだけで、

  • ログイン時の observeQuery で Unauthorized が完全に消える
  • onUpdate / onDelete も正常に購読
  • 初期ロード時の大量ログ(Unauthorized)がゼロに

となりました。


📌 Amplify Gen2 の注意点:owner は自動生成されない

Amplify(旧 Codegen / GraphQL Transformer)では
@auth(rules:[{allow: owner}] を書くと自動で owner フィールドを作ってくれていました。

しかし、Amplify Gen2(Data / a.model())は自動生成しません。

そのため、owner 認可を使う場合は 明示的に owner フィールドを作る必要があります。


🔍 なぜ listBoardPosts が Unauthorized になるの?

observeQuery は内部的に

  1. listBoardPosts(初期 LIST)
  2. onCreate
  3. onUpdate ← ここで owner 判定が必要
  4. onDelete

をすべて購読します。

その中で owner を参照できないため、

  • “この update を購読してよいユーザーか”
  • “owner は誰なのか”

が判断できず、Amplify がファインチューニングできずに Unauthorized に落ちる仕組みです。


🛠 調査するときのポイント

① GraphQL のログを CloudWatch で確認

AppSync → Logs → field resolver errors を見ると Unauthorized が出ています。

authMode を強制指定して切り分け

observeQuery({ authMode: 'identityPool' })

だと動く → owner が原因

③ model の auth ルールとフィールドの一致を確認

owner 認可ルールとフィールド仕様がズレると確実に落ちる。


🎉 まとめ

原因 owner フィールドを持たずに allow.owner() を使っていた
症状 observeQuery で onUpdate 時に Unauthorized が発生
解決 model に owner: a.string() を追加するだけ

Amplify Gen2 では、旧来の GraphQL Transformer のように
owner フィールドを自動生成してくれないため、
owner 認可を使う=owner フィールドを明示的に書く必要がある
という点が最大のハマりポイントになります。

同じ問題で悩んでいる方の助けになれば嬉しいです!

Tailwind CSS v4 に対応していない eslint-plugin-tailwindcss をどう使う?(続編)

先週のブログ記事(2025/03/11公開)では、Tailwind CSS v4にアップデートする際に、eslint-plugin-tailwindcssが対応しておらず、Lintエラーが発生することに触れました。

本記事ではその続編として、v4アップデート時に特に問題となる4つのTailwind公式プラグインについて詳しく解説します。なぜエラーになるのか?どんな用途なのか?今後の見通しはどうなのか?といった疑問にお答えします。


問題の背景:v3に依存している4つのプラグイン

Tailwind CSS v4へ移行しようとすると、以下の4つの公式プラグインが v3 に依存しているため、npmやyarnのインストール時に peer dependency エラー が発生します:

  • @tailwindcss/aspect-ratio
  • @tailwindcss/forms
  • @tailwindcss/line-clamp
  • @tailwindcss/typography

それぞれの役割

@tailwindcss/aspect-ratio

  • 用途:要素の縦横比を指定(例:16:9、1:1など)。画像や動画の表示レイアウトで頻出。

@tailwindcss/forms

  • 用途:フォーム要素(inputやselectなど)のデフォルトスタイルをリセットし、より一貫性のあるUIを実現。

@tailwindcss/line-clamp

  • 用途:テキストを指定行数で切り詰め(line-clamp-3など)、溢れる文章を「...」で省略。

@tailwindcss/typography

  • 用途:ブログや記事など、リッチテキストコンテンツのスタイルを整える(通称"Typography plugin"、proseクラスなど)。

作者とメンテナンス状況

上記の4つのプラグインは、すべて Tailwind CSSの開発元である Tailwind Labs によって開発・メンテナンスされています。

プラグインGitHub上で管理されており、最新のリリースやTailwind v4への対応状況もそちらで確認できます。

現在(2025年3月)時点では、いずれも peerDependencies に Tailwind CSS v3 を指定しており、v4との互換性は確認されていません。

そのため、eslint-plugin-tailwindcss がv4対応に向けた開発を進めようとしても、これらの依存パッケージが足かせになっているという状況です。


今後の対応に向けて

Tailwind Labsの動きとして、Tailwind CSS v4の安定化が進めば、これらのプラグインも段階的に v4 対応されていくと見込まれます。

また、eslint-plugin-tailwindcssの作者も GitHub Issue #325 にて「v4対応には時間がかかる」としながらも、前向きな姿勢を見せています。

ウォッチしておきたいGitHubリポジトリ


まとめ

Tailwind CSS v4を導入しようとした際、eslint-plugin-tailwindcssのv3依存が問題になる場合、根本的な原因は4つの公式プラグインがまだv3に依存している点にあります。

現時点では、以下のようなアプローチが考えられます:

  • 依存関係を無理やり通す(--legacy-peer-deps
  • .eslintrcで一部Lintルールを無効にする
  • Lintプラグインを一時的に外す

そして何より、これらのプラグインのアップデート状況を GitHubでウォッチ しておくことが、v4環境での安定運用への近道になります。

Nuxt3 + nuxt-eslint ユーザーのための Tailwind CSS v4 フォーマット統一ガイド

ESLint Stylistic + eslint-plugin-tailwindcss で統一していたのに…

Nuxt3 を使って開発している皆さん、Tailwindのクラス順のフォーマットってどうしていますか?

私はこれまで、nuxt-eslint モジュールを使い、eslint-plugin-tailwindcss を導入して、Tailwind CSS v3 のクラス順を ESLint Stylistic でしっかり統一していました。

ところが、Tailwind CSS v4 にアップデートした途端、問題が発生。

いつものように ESLint を実行すると、eslint-plugin-tailwindcss に関するエラーが出るようになりました。

「おや?バージョンの問題かな?」と思い、eslint-plugin-tailwindcss を再インストールしようとしたところ…

$ npm i -D eslint-plugin-tailwindcss
npm error code ERESOLVE
npm error ERESOLVE unable to resolve dependency tree
npm error
npm error While resolving: nuxt-app@undefined
npm error Found: tailwindcss@4.0.12
npm error node_modules/tailwindcss
npm error   tailwindcss@"^4.0.12" from the root project
npm error
npm error Could not resolve dependency:
npm error peer tailwindcss@"^3.4.0" from eslint-plugin-tailwindcss@3.18.0
npm error node_modules/eslint-plugin-tailwindcss
npm error   dev eslint-plugin-tailwindcss@"*" from the root project
npm error
npm error Fix the upstream dependency conflict, or retry
npm error this command with --force or --legacy-peer-deps
npm error to accept an incorrect (and potentially broken) dependency resolution.
npm error
npm error
npm error For a full report see:

エラー発生! 依存関係の問題でインストールできない。

「何が起こっているんだ…?」

調べてみると、公式 GitHub Issue #325 にて、2025年3月11日現在、eslint-plugin-tailwindcss は Tailwind CSS v4 を正式にはサポートしていない ことが確認できました。

「なるほど、だからエラーが出たのか…!」

これは困った。クラスの並び順がカオスになり、レビュー時の指摘も増加。ESLint で統一できていたスタイルが崩壊し、手動で修正するのは現実的ではありません。

「Issue の内容を見ると、すぐには対応されなさそうだ…」

すぐに解決できる方法がなさそうなので、別の方法を探ることに。

できれば ESLint だけで完結したかったけど、、

幸いにも、nuxt-eslint公式ドキュメント で Prettier の利用が推奨されていることがわかり、ESLint と併用しても問題ないことを確認できました。そこで、Prettier を導入 することにしました。

nuxt-eslint のドキュメントを確認した結果、Prettier を使うのが最適と判断し、クラスの並び順を統一することにしました。

インストール手順

npm install -D prettier prettier-plugin-tailwindcss

Prettier の設定

.prettierrc に以下の設定を追加します。

{
  "plugins": ["prettier-plugin-tailwindcss"]
}

コードのフォーマット

npx prettier --write .

これで Tailwind CSS v4 のクラス順が統一され、少なくとも「クラスがぐちゃぐちゃで読みにくい問題」は解決しました。

nuxt-eslint公式ドキュメント にも Prettier の利用が記載されており、ESLint と組み合わせることでコードのスタイルを一貫させることができます。Prettier は Tailwind CSS のクラス順を整理し、ESLint はその他のスタイルルールをチェックする役割を担うため、併用するのが最適です。

さらに、nuxt-eslint を導入すれば ESLint のチェックは問題なく機能します。Prettier はクラスの順序整理に特化し、ESLint は他のコードスタイルやルールをチェックする役割として共存させるのがベストプラクティスです。

将来に期待! ESLint の CSS リンティングサポート

そんな中、2025年2月に ESLint が公式に CSS のリンティングをサポート することを発表。

将来的には Tailwind CSSフォーマットチェックESLint だけで完結する可能性 があります。

例えば、以下のような設定で Tailwind の構文を解析できるようになります。

import { tailwindSyntax } from "@eslint/css/syntax";

ただし、現時点では Tailwind CSS v4 のクラス順を自動で整理するところまではサポートされていません。

まとめ

Nuxt3 + nuxt-eslint ユーザーとして、現時点で Tailwind CSS v4 のフォーマットを統一するための最適解は次のとおりです。

  1. Prettier(prettier-plugin-tailwindcss)でクラス順序を整理する(必須!)
  2. ESLint の CSS リンティングサポートに期待しつつ、今後の改善を待つ

これらを組み合わせることで、Nuxt3 + Tailwind CSS v4 の環境を快適に保ち、開発効率を向上させることができます。


「早く ESLint だけで完結してほしい!」 という願いを抱きつつ、今は Prettier に頼るのが最善策です。

この記事が、私と同じように Tailwind CSS v4 のフォーマット問題で困っている Nuxt3 + ESLint ユーザーの助けになれば幸いです!