MeWrite Docs

OAuth2: invalid_grant

OAuth2認可エラーの解決方法

概要

OAuth2のトークン交換時に認可グラントが無効な場合に発生するエラーです。

エラーメッセージ

1
2
3
4
{
  "error": "invalid_grant",
  "error_description": "The provided authorization grant is invalid, expired, or revoked"
}

原因

  1. 認可コードの期限切れ: コードの有効期限が短い
  2. コードの再利用: 認可コードは1回限り
  3. redirect_uriの不一致: 認可時と異なるURI
  4. リフレッシュトークン無効: 取り消しまたは期限切れ

解決策

1. PKCEを使用した認可コードフロー(推奨)

OAuth 2.1ではPKCE(Proof Key for Code Exchange)が必須です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// PKCE用のcode_verifierとcode_challengeを生成
function generatePKCE() {
  const verifier = crypto.randomBytes(32).toString('base64url');
  const challenge = crypto
    .createHash('sha256')
    .update(verifier)
    .digest('base64url');
  return { verifier, challenge };
}

// 認可リクエスト
const { verifier, challenge } = generatePKCE();
// verifierをセッションに保存
req.session.codeVerifier = verifier;

const authUrl = `https://oauth.example.com/authorize?` +
  `client_id=${CLIENT_ID}&` +
  `redirect_uri=${encodeURIComponent(REDIRECT_URI)}&` +
  `response_type=code&` +
  `code_challenge=${challenge}&` +
  `code_challenge_method=S256`;

// トークン交換(PKCEあり)
app.get('/callback', async (req, res) => {
  const { code } = req.query;
  const codeVerifier = req.session.codeVerifier;

  const response = await fetch('https://oauth.example.com/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'authorization_code',
      code,
      redirect_uri: REDIRECT_URI,
      client_id: CLIENT_ID,
      code_verifier: codeVerifier  // PKCEのverifier
    })
  });
});

2. 認可コードを即座に交換

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 認可コード受信後すぐにトークンを取得(10分以内が目安)
app.get('/callback', async (req, res) => {
  const { code } = req.query;

  const response = await fetch('https://oauth.example.com/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'authorization_code',
      code,
      redirect_uri: 'http://localhost:3000/callback',
      client_id: CLIENT_ID,
      client_secret: CLIENT_SECRET
    })
  });
});

3. redirect_uriを一致させる

1
2
3
4
5
6
7
// 認可リクエスト
const authUrl = `https://oauth.example.com/authorize?` +
  `client_id=${CLIENT_ID}&` +
  `redirect_uri=${encodeURIComponent('http://localhost:3000/callback')}&` +
  `response_type=code`;

// トークンリクエストでも同じredirect_uri

4. リフレッシュトークンの更新

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
async function refreshToken() {
  try {
    const response = await fetch('https://oauth.example.com/token', {
      method: 'POST',
      body: new URLSearchParams({
        grant_type: 'refresh_token',
        refresh_token: storedRefreshToken,
        client_id: CLIENT_ID
      })
    });

    if (!response.ok) {
      // 再認可が必要
      redirectToAuth();
    }
  } catch (err) {
    redirectToAuth();
  }
}

よくある間違い

  • 認可コードの使い回し
  • トークン保存時の暗号化忘れ
  • PKCEを使用していない(OAuth 2.1では必須)
  • code_verifierとcode_challengeの不一致

参考リンク

関連エラー

Security の他のエラー

最終更新: 2025-12-09