Nuxt 3 + Google OAuth (PKCE) を SPA（CSR）構成で動かす最小構成ガイド

:::note info
✍ 本記事は AI（ChatGPT）を活用して下書きを作成し、筆者が検証・編集のうえ公開しています。
:::

## TL;DR
- Google OAuth + PKCE を Nuxt3 CSR で使うと `client_secret` 問題に詰まる  
- ミニ BFF (Express / Spring でも可) に `/auth/exchange` を置けば解決  
- フルサンプルは GitHub → [nuxt3-google-oauth-pkce-sample](https://github.com/RuumaLilja/nuxt3-google-oauth-pkce-sample)

## 背景
Google の OAuth PKCE フローは Web アプリ登録だと client_secret が必須。CSR 環境で動かすにはBFFでトークン交換を代行させるのが現実的な解法です。この記事では Nuxt 3 CSR + Express マイクロサービス構成のサンプルをステップごとに紹介します。
あるプロジェクトで Nuxt 3 CSR アプリとバックエンド API を組み合わせる際、Google の PKCE フローで client_secret が必須となり動作しない問題が発生しました。本記事では、それを解決するための Express マイクロサービス（BFF）的な構成を解説します。

## 目次
1. 背景 ― なぜシークレット問題にハマるのか

2. SPA（CSR）構成 で取り得る 4 つのアーキテクチャ

3. 実装ステップ

4. Google Cloud 側の設定

5. Express マイクロサービス (コード → トークン交換)

6. Nuxt 3 CSR クライアント

7. 付録 A. デプロイ & サーバーレス化 Tips

8. まとめ・今後の改善ポイント

## 1. 背景 ― Google PKCE で client_secret が必須になる理由

- RFC 7636 (PKCE) は本来 Public Client 向け → シークレット不要のはず

- Google は Web アプリ 登録時に「クライアント認証は必須」という実装ポリシー

- CSR 単体ではトークンエンドポイントを呼べず、BFF などでシークレットを隠蔽する必要がある

## 2. フロント単体 (CSR) で取り得る 4 つのアーキテクチャ

| パターン | 概要 | Pros | Cons |
| ---- | ---- |---- | ---- |
| A. CSR + マイクロサービス (今回) | Express/Lambda がトークン交換 | Google 対応可静的ホスティングと親和性◎ | サーバー管理が必要 |
| B. SSR + BFF | Nuxt SSR 内でシークレット保持 | 同一オリジンで完結 | SSR 運用コスト |
| C. Public Client プロバイダ | Auth0 / Azure AD など | フロントだけで完結 | Google 利用不可 |
| D. Implicit Flow | レガシー方式 | サーバー不要 | セキュリティ弱い |

 本記事では A: CSR + ミニ Express (Spring でも可) を実装。

### 2.1 他の実装パス
- **Spring Bootに `spring-boot-starter-oauth2-client` を組み込む**  
  → バックエンドが Java の場合はコード→トークン交換を Spring Security が代行できる
特定プロバイダのライブラリに依存するため今回は不採用

## 3. 実装ステップ
### 3.1 Google Cloud 設定
1. OAuth 同意画面: アプリ名・ドメイン登録
2. 認証情報 → OAuth 2.0 クライアント ID
   - アプリケーションの種類: ウェブアプリケーション
   - 名前: 任意
   - 承認済みの JavaScript 生成元: http://localhost:3000
    - 承認済みのリダイレクトURI: http://localhost:3000/auth/callback

3. client_id と client_secret を控える

### 3.2 Express BFF (server/)

- 依存: `express cors dotenv node-fetch qs`
- 重要ポイント
  - `express.urlencoded()` で form-urlencoded をパース
  - `/auth/exchange` で `client_secret` を付けてトークン取得
```javascript
router.post('/', async (req, res, next) => {
  const { code, code_verifier } = req.body
  const conf = await fetch(process.env.OIDC_WELL_KNOWN).then(r=>r.json())
  const token = await fetch(conf.token_endpoint, {
    method:  'POST',
    headers: {'Content-Type':'application/x-www-form-urlencoded'},
    body: qs.stringify({
      grant_type:'authorization_code',
      client_id:process.env.OIDC_CLIENT_ID,
      client_secret:process.env.OIDC_CLIENT_SECRET,
      redirect_uri:process.env.OIDC_REDIRECT_URI,
      code, code_verifier
    })
  }).then(r=>r.json())
  res.json(token)
})
```
### 3.3 Nuxt 3 CSR (client/)
- 依存: `@pinia/nuxt oidc-client-ts`
    
- `plugins/oidc.client.js` で token_endpoint を `apiBase + /auth/exchange` に上書き
    
- `handleCallback()` で `signinCallback()` → トークンとプロフィールをストアへ
    

```javascript
export const handleCallback = async () => {
  const user = await mgr.signinCallback()
  store.setTokens({ access_token: user.access_token })
  store.setUser(user.profile)
  await navigateTo('/')
}
```

### 3.4 動作確認

```bash
# server
npm run dev
# client
npm run dev
```

## 付録 A. デプロイ & サーバーレス化 Tips


|レイヤー|例|ポイント|
|---|---|---|
|フロント|Netlify / Vercel / Cloudflare Pages|`nuxt generate` 出力で即デプロイ|
|BFF|Vercel Functions / Cloudflare Workers / AWS Lambda|`.env` シークレットはプラットフォーム管理|

---

## 4. まとめ・今後の改善

- **リフレッシュトークン運用**：httpOnly Cookie + Token Rotation
    
- **セキュリティ強化**：`express-rate-limit` & Helmet & CORS 制限
    
- **別 ID プロバイダ**：Microsoft identity platform 等ならフロント完結も可能
    
- **SSR/BFF 移行**：必要に応じて Nuxt Nitro に統合するとオリジンを一本化できる
    

## 5. ソースコード

GitHub にて今回紹介した構成の全コードを公開しています：
 [nuxt3-google-oauth-pkce-sample - GitHub リポジトリ](https://github.com/RuumaLilja/nuxt3-google-oauth-pkce-sample)
  静的ホスティング × Google OAuth の“微妙な壁”にハマった方の助けになれば幸いです！