はじめに
Node.js プロジェクトで JWT (JSON Web Token) の検証を行う際、多くの開発者が従来の jsonwebtoken
ライブラリから、よりモダンな jose
ライブラリへの移行を検討します。しかし、この移行プロセス中に、公開鍵のフォーマットが互換性のない問題に遭遇することがよくあります。この記事では、私が移行中に直面した問題と、その最終的な解決策について共有します。
結論:crypto
モジュールで PEM 形式を統一する
TL;DR: jsonwebtoken
から jose
に移行する際、 Invalid keyData
または wrong tag
エラーが発生した場合、Node.js の crypto
モジュールを使用して PEM 形式を標準化する必要があります。
import * as crypto from 'crypto';
import * as jose from 'jose';
// crypto を使用して PEM 形式を標準化
const key = crypto.createPublicKey(ssoPublicKeyPem);
const fixedPem = key.export({
type: 'spki',
format: 'pem'
});
// 標準化された PEM を使用して jose で検証
const publicKey = await jose.importSPKI(fixedPem, 'RS256');
const { payload } = await jose.jwtVerify(token, publicKey);
少し苦戦したデバッグ
従来の jsonwebtoken
による実装
当初、私は jsonwebtoken
ライブラリを使って JWT 検証を行っていました。そのコードは非常にシンプルでした。
import jwt from 'jsonwebtoken';
const ssoPublicKey = process.env.SSO_PUBLIC_KEY.replace(/\\n/g, '\n') || '';
const payload = jwt.verify(token, ssoPublicKey);
console.log('JWT 検証成功、Payload:', payload);
この実装は問題なく動作していました。
jose
への移行時に発生したエラー
コードを jose
ライブラリに移行しようとした際、最初の実装は以下のようになりました。
import * as jose from 'jose';
const ssoPublicKeyPem = process.env.SSO_PUBLIC_KEY.replace(/\\n/g, '\n') || '';
const publicKey = await jose.importSPKI(ssoPublicKeyPem, 'RS256');
しかし、実行時に以下のエラーが発生しました。
DOMException [DataError]: Invalid keyData
at Object.rsaImportKey (node:internal/crypto/rsa:221:15)
at SubtleCrypto.importKey (node:internal/crypto/webcrypto:615:10)
...
[cause]: Error: error:068000A8:asn1 encoding routines::wrong tag
PEM のヘッダ/フッタの変更を試みる
エラーメッセージを見て、まず PEM 形式の問題を疑いました。環境変数内の公開鍵が以下のようになっていることに気づきました。
SSO_PUBLIC_KEY="-----BEGIN RSA PUBLIC KEY-----\n...
そこで、ヘッダとフッタを RSA PUBLIC KEY
から PUBLIC KEY
に変更することを試みました。
// ヘッダ/フッタ形式の変更を試みる
let ssoPublicKeyPem = process.env.SSO_PUBLIC_KEY.replace(/\\n/g, '\n') || '';
ssoPublicKeyPem = ssoPublicKeyPem
.replace('-----BEGIN RSA PUBLIC KEY-----', '-----BEGIN PUBLIC KEY-----')
.replace('-----END RSA PUBLIC KEY-----', '-----END PUBLIC KEY-----');
しかし、このように変更しても同じエラーが発生しました。
2つのライブラリの内部処理メカニズムの差異
jsonwebtoken
の処理方法
jsonwebtoken
ライブラリは内部で、より緩やかな鍵解析メカニズムを使用しています。これにより、以下のことが可能です。
- 様々な PEM 形式(
RSA PUBLIC KEY
、PUBLIC KEY
など)を自動的に処理 - フォーマットエラーに対する一定の許容性
- 内部でいくつかの形式変換と標準化を実行
jose
ライブラリの処理方法
一方、jose
ライブラリはより厳格です。
- Web Crypto API 標準に厳密に従って鍵を解析
- PEM 形式が標準の SPKI 形式(
-----BEGIN PUBLIC KEY-----
)であることを要求 - ASN.1 エンコーディングに対してより厳格な要件
- Node.js の Web Crypto API を直接呼び出し
なぜ crypto
モジュールが必要なのか
問題の根本原因は、PEM のヘッダ/フッタを手動で変更したとしても、内部の ASN.1 エンコーディング構造が jose
ライブラリの要件を満たしていない可能性がある点にあります。
Node.js の crypto.createPublicKey()
メソッドは、以下のことを実現します。
-
様々な形式の公開鍵を自動的に識別(
RSA PUBLIC KEY
、PUBLIC KEY
など) - 内部エンコーディング構造を標準化し、ASN.1 エンコーディングが標準に準拠していることを保証
-
標準の SPKI 形式を出力。これこそが
jose.importSPKI()
が必要とするものです。
// crypto.createPublicKey の処理プロセス:
// 1. 元の PEM を解析(どんな形式でも)
// 2. 公開鍵の数学的パラメータを抽出
// 3. 標準の SPKI 形式に再エンコード
// 4. 標準の PEM 文字列を出力
const key = crypto.createPublicKey(originalPem);
const standardizedPem = key.export({
type: 'spki', // SPKI 形式での出力を指定
format: 'pem' // PEM 文字列での出力を指定
});
まとめ
修正後の jose
による実装
import dotenv from 'dotenv';
dotenv.config({ path: '../.env' }); // .env ファイルのパスは適宜調整してください
import * as jose from 'jose';
import * as crypto from 'crypto';
// 環境変数内の公開鍵を処理
const ssoPublicKeyPem = process.env.SSO_PUBLIC_KEY.replace(/\\n/g, '\n') || '';
console.log(`debug:ssoPublicKeyPem: ${ssoPublicKeyPem}`);
// Node.js の crypto モジュールを使用して PEM 形式を標準化
const key = crypto.createPublicKey(ssoPublicKeyPem);
const fixedPem = key.export({
type: 'spki',
format: 'pem'
});
console.log(`debug:fixedPem: ${fixedPem}`);
// JWT トークン検証 (例として dummy token を使用)
// 実際のアプリケーションでは検証対象の JWT を渡してください
const token = 'YOUR_JWT_TOKEN_HERE'; // ここに検証したい JWT を入れます
async function verifyToken() {
try {
// 標準化された PEM 形式から公開鍵をインポート
const publicKey = await jose.importSPKI(fixedPem, 'RS256');
// JWT を検証
const { payload, protectedHeader } = await jose.jwtVerify(token, publicKey);
console.log('JWT 検証成功、Protected Header:', protectedHeader);
console.log('JWT 検証成功、Payload:', payload);
return payload;
} catch (error) {
console.error('JWT 検証失敗:', error);
throw error;
}
}
// 関数の呼び出し (テスト用)
verifyToken();
jsonwebtoken
から jose
へ移行する際、重要なのは、両ライブラリの公開鍵形式に対する要求の違いを理解することです。jose
ライブラリはより厳格で標準化されており、これによって多少の複雑性が増しますが、その分、パフォーマンス向上とより厳格なセキュリティを実現します。
crypto.createPublicKey()
を用いた形式の標準化は、公開鍵形式の互換性を確保し、移行プロセスをよりスムーズにするための確実な解決策です。
参考資料
あとがき
この Qiita 記事が、jsonwebtoken
から jose
への移行でお困りの方々の一助となれば幸いです。初めて技術的な記事を書くので、本当にこのテーマで大丈夫か自信がないです。もし疑問点があれば、お気軽にコメントください。
ちなみに、この記事の本文はGitHub Copilotを使って書いてもらったものです。私がデバッグする際、Google検索に加えて、Copilotにも問題解決の手助けをしてもらったからです。Copilotは、私が遭遇したエラーも記録してくれて、指定したアウトラインに沿って自動的に.mdファイルに書き込むことができます。出来上がった記事を自分で読み直し、不要だと感じた比較表の一部を削除したほかは、ほとんど手を加えていません。本当に助かりました。