MeWrite Docs

OpenID Connect: id_token validation failed

OpenID Connectのid_tokenの検証に失敗するエラー

概要

id_token validation failed は、OpenID Connect(OIDC)認証フローでIdentity Providerから受け取ったid_tokenの検証に失敗した際に発生するエラーです。JWT署名の検証失敗、issuerの不一致、有効期限切れなどが原因です。

エラーメッセージ

Error: id_token validation failed: nonce mismatch
Error: Invalid id_token: issuer does not match
Expected: https://accounts.google.com
Received: https://login.example.com
JsonWebTokenError: jwt expired
Error: id_token validation failed: invalid signature
OIDCError: id_token issued in the future (iat claim)

原因

  1. nonce の不一致: 認証リクエスト時のnonceとid_tokenのnonceが異なる
  2. issuer の不一致: id_tokenのissクレームが期待するIdPと異なる
  3. 有効期限切れ: id_tokenのexpクレームが現在時刻を過ぎている
  4. 署名の検証失敗: JWKSから取得した公開鍵で署名が検証できない
  5. audience の不一致: id_tokenのaudクレームがClient IDと異なる
  6. 時刻のずれ: サーバー間の時刻ずれによりiat/exp検証が失敗

解決策

1. Node.js(openid-client)

 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
40
const { Issuer, generators } = require('openid-client');

// IdP のディスカバリー
const issuer = await Issuer.discover('https://accounts.google.com');
const client = new issuer.Client({
  client_id: process.env.OIDC_CLIENT_ID,
  client_secret: process.env.OIDC_CLIENT_SECRET,
  redirect_uris: ['https://app.example.com/callback'],
  response_types: ['code']
});

// 認証リクエスト(nonce を生成して保存)
app.get('/login', (req, res) => {
  const nonce = generators.nonce();
  const state = generators.state();
  req.session.nonce = nonce;
  req.session.state = state;

  const authUrl = client.authorizationUrl({
    scope: 'openid email profile',
    nonce: nonce,
    state: state
  });
  res.redirect(authUrl);
});

// コールバック(nonce を検証に使用)
app.get('/callback', async (req, res) => {
  const params = client.callbackParams(req);
  const tokenSet = await client.callback(
    'https://app.example.com/callback',
    params,
    {
      nonce: req.session.nonce,  // 保存した nonce を渡す
      state: req.session.state
    }
  );

  console.log('id_token claims:', tokenSet.claims());
});

2. Python(authlib)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from authlib.integrations.flask_client import OAuth

oauth = OAuth(app)
oauth.register(
    name='google',
    server_metadata_url='https://accounts.google.com/.well-known/openid-configuration',
    client_id=os.getenv('GOOGLE_CLIENT_ID'),
    client_secret=os.getenv('GOOGLE_CLIENT_SECRET'),
    client_kwargs={'scope': 'openid email profile'}
)

@app.route('/callback')
def callback():
    try:
        token = oauth.google.authorize_access_token()
        # id_token の検証は authlib が自動で行う
        user_info = token.get('userinfo')
        if not user_info:
            user_info = oauth.google.userinfo()
        return create_session(user_info)
    except Exception as e:
        # エラー詳細をログに記録
        app.logger.error(f'OIDC validation error: {e}')
        return redirect('/login?error=oidc_failed')

3. id_token の手動検証

 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
const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');

const client = jwksClient({
  jwksUri: 'https://accounts.google.com/.well-known/jwks.json',
  cache: true,
  rateLimit: true
});

function getSigningKey(header, callback) {
  client.getSigningKey(header.kid, (err, key) => {
    if (err) return callback(err);
    callback(null, key.getPublicKey());
  });
}

function verifyIdToken(idToken, expectedNonce) {
  return new Promise((resolve, reject) => {
    jwt.verify(idToken, getSigningKey, {
      algorithms: ['RS256'],
      issuer: 'https://accounts.google.com',
      audience: process.env.OIDC_CLIENT_ID,
      clockTolerance: 30 // 30秒の時刻ずれを許容
    }, (err, decoded) => {
      if (err) return reject(err);

      // nonce の検証
      if (decoded.nonce !== expectedNonce) {
        return reject(new Error('nonce mismatch'));
      }

      resolve(decoded);
    });
  });
}

4. 時刻同期の確認

1
2
3
4
5
6
7
8
# サーバーの時刻を確認
date -u

# NTP 同期の確認
timedatectl status

# 時刻を強制同期
sudo ntpdate pool.ntp.org

5. デバッグ: id_token の中身を確認

1
2
3
4
5
# id_token のペイロードをデコード
echo "eyJhbGciOiJSUzI1NiIs..." | \
  cut -d'.' -f2 | \
  base64 -d 2>/dev/null | \
  jq .
1
2
3
4
5
6
7
8
// Node.js で id_token をデコード(検証なし、デバッグ用)
const decoded = jwt.decode(idToken, { complete: true });
console.log('Header:', decoded.header);
console.log('Payload:', decoded.payload);
console.log('Issuer:', decoded.payload.iss);
console.log('Audience:', decoded.payload.aud);
console.log('Expiry:', new Date(decoded.payload.exp * 1000));
console.log('Nonce:', decoded.payload.nonce);

関連エラー

Security の他のエラー

最終更新: 2026-02-03