05 · トークン検証とセキュリティ
05 · トークン検証とセキュリティ
プッシュであれプルであれ、最終的に開発者は一枚の wsa(HS256 JWT)を受け取ります。本編はすべての接続側にとって必読 です——検証が甘い = ID が偽造可能ということです。
必須検証チェックリスト
wsa を取得したら、拡張アプリのバックエンドサービスで 順番に検証します(verifyWsa はすべてを内蔵済み):
- 署名:プラットフォームが配布/登録で得たシークレットで HS256 検証します。失敗 → 拒否。
exp:現在時刻 ≤exp(わずかな leeway を含む)。expは 必ず存在 しなければなりません——expのないトークンは直接拒否すべきです(さもなければ永久に期限切れにならないのと同じです)。iss:gptbots-workspaceと一致しなければなりません。aud:対象拡張アプリの host と一致しなければなりません。これは「トークンを盗んで別のアプリに打ち込む」ことを防ぐ要の防衛線であり、省略できません。iat/nbf(存在する場合):未来(leeway を超える)であってはなりません。「発行時刻を遠い未来に設定した」トークンが長期間使えることを防ぎます。workspace_idによるテナント分離:拡張が複数ワークスペースの分離を行っている場合、リクエストをそのworkspace_idの配下に振り分け、テナント跨ぎのアクセスを禁止します。- ランディング後すぐに URL 内の
wsa/codeを消去:history.replaceStateで、ユーザーが URL をコピーしたときにトークンまで一緒に共有してしまうのを防ぎます。 - 自前のセッションを確立:拡張アプリ自身の session/cookie に置き換え、以降のリクエストで
wsaに依存 しない ようにします——5 分で期限切れになります。
公式 SDK の
verifyWsaは、expの存在を必須とし、iat/nbfの未来方向のずれを検証し、定数時間での署名比較とアルゴリズムのホワイトリスト(デフォルトはHS256のみ、alg混同攻撃を防止)を行います。これを使えば 1〜5 は自動的に満たされます。
SDK での検証を推奨
import { verifyWsa, WsaVerificationError } from '@gptbots/workspace-extension-verify';
try {
const id = verifyWsa(wsa, {
secret: process.env.EXTENSION_APP_SECRET,
audience: 'app.example.com',
});
// id: { accountId, role, workspaceId, username?, email?, avatar?, appName?, issuedAt?, expiresAt? }
} catch (e) {
if (e instanceof WsaVerificationError) {
// e.code: InvalidToken | InvalidSignature | Expired | NotYetValid
// | WrongIssuer | WrongAudience | MissingClaim | UnsupportedAlgorithm
}
}
import { verifyWsa, WsaVerificationError } from '@gptbots/workspace-extension-verify';
try {
const id = verifyWsa(wsa, {
secret: process.env.EXTENSION_APP_SECRET,
audience: 'app.example.com',
});
// id: { accountId, role, workspaceId, username?, email?, avatar?, appName?, issuedAt?, expiresAt? }
} catch (e) {
if (e instanceof WsaVerificationError) {
// e.code: InvalidToken | InvalidSignature | Expired | NotYetValid
// | WrongIssuer | WrongAudience | MissingClaim | UnsupportedAlgorithm
}
}
このコードブロックをポップアップで表示
完全なオプションとエラーコードは SDK APIリファレンス を参照してください。
SDK を使わない検証(多言語)
wsa は標準的な HS256 JWT なので、どの JWT ライブラリでも検証できます。必ず iss / aud 検証を明示的に有効化し、algorithms=['HS256'] に固定してください。
Java(auth0 java-jwt)
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(SHARED_SECRET))
.withIssuer("gptbots-workspace")
.withAudience("app.example.com")
.acceptLeeway(30) // 30s の時計のずれを許容
.build();
DecodedJWT jwt = verifier.verify(wsaParam);
String userId = jwt.getSubject();
String workspaceId = jwt.getClaim("workspace_id").asString();
String role = jwt.getClaim("role").asString();
String username = jwt.getClaim("username").asString(); // null の可能性あり
String email = jwt.getClaim("email").asString(); // null の可能性あり
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(SHARED_SECRET))
.withIssuer("gptbots-workspace")
.withAudience("app.example.com")
.acceptLeeway(30) // 30s の時計のずれを許容
.build();
DecodedJWT jwt = verifier.verify(wsaParam);
String userId = jwt.getSubject();
String workspaceId = jwt.getClaim("workspace_id").asString();
String role = jwt.getClaim("role").asString();
String username = jwt.getClaim("username").asString(); // null の可能性あり
String email = jwt.getClaim("email").asString(); // null の可能性あり
このコードブロックをポップアップで表示
Node.js(jsonwebtoken)
const jwt = require('jsonwebtoken');
const payload = jwt.verify(wsaParam, SHARED_SECRET, {
algorithms: ['HS256'],
issuer: 'gptbots-workspace',
audience: 'app.example.com',
clockTolerance: 30,
});
const { sub: userId, workspace_id, role, username, email, avatar } = payload;
const jwt = require('jsonwebtoken');
const payload = jwt.verify(wsaParam, SHARED_SECRET, {
algorithms: ['HS256'],
issuer: 'gptbots-workspace',
audience: 'app.example.com',
clockTolerance: 30,
});
const { sub: userId, workspace_id, role, username, email, avatar } = payload;
このコードブロックをポップアップで表示
Python(PyJWT)
import jwt
payload = jwt.decode(
wsa_param,
SHARED_SECRET,
algorithms=["HS256"],
issuer="gptbots-workspace",
audience="app.example.com",
leeway=30,
)
user_id = payload["sub"]
workspace_id = payload["workspace_id"]
role = payload["role"]
import jwt
payload = jwt.decode(
wsa_param,
SHARED_SECRET,
algorithms=["HS256"],
issuer="gptbots-workspace",
audience="app.example.com",
leeway=30,
)
user_id = payload["sub"]
workspace_id = payload["workspace_id"]
role = payload["role"]
このコードブロックをポップアップで表示
シークレットのエンコードに関する注意:HS256 はシークレット文字列をそのまま UTF-8 バイトとして HMAC key に使います。Tier 2 の per-app シークレットは
wext_+64 hex のような形式ですが、文字列をそのまま シークレットとして渡してください(改めて hex デコードしないこと)。プラットフォームの署名側と一致させます。
セキュリティ要件(一つずつ実施)
- シークレットがセキュリティモデルのすべて。現在の HS256 は対称鍵です:一度漏洩すれば、誰でも任意のワークスペースユーザーの ID を偽造して接続先の拡張アプリに打ち込めます。バックエンドにのみ配置(環境変数/KMS)し、フロントエンドコード、git リポジトリ、ログ、クライアント設定に絶対に書き込まないでください。
- JWT が URL に露出する範囲(プッシュのみ):query string はブラウザ履歴、Web サーバーのアクセスログ、CDN キャッシュログ、Referer に記録されます。たとえ記録されても、5 分以内に入手されれば依然として悪用可能です。ランディング後すぐに
history.replaceStateで消去 してください(consumeHandoffはデフォルトで実施済み)。プル(M-Auth)は元来wsaを URL に入れないため、より安全です。 wsaを子リソースに透過的に渡さない。自前のセッションに置き換えた後、以降の XHR/fetch/img には一切生のwsaを付けないでください。さもなければ各子リソースの Referer に出現します。- 時計の同期。HS256 は
expを厳密に判定するため、サーバーの時計は NTP 同期が必要です。例では 30s のleewayで小幅なずれを許容していますが、分単位まで広げないでください。 roleはデフォルト権限マッピングとしてのみ。それはそのワークスペースにおけるユーザーのロールを反映するだけで、あなたのアプリ内の権限を表しません。あなたのアプリは独自の権限モデルを維持します。- マルチテナント分離。常に
workspace_idをデータ分離キーとして使い、A ワークスペースのユーザーが B のデータを読めないようにします。 - シークレットのローテーション/失効の緊急対応:スペース管理でアプリの「シークレットをローテーション」すると、新しいシークレットが生成され一度だけ表示されます。その後、新しいシークレットでバックエンドを再設定してください。
5. フロントエンドの信頼境界(重要)
- フロントエンドは表示用プレースホルダーのみを担い、認可判断はしない。フロントエンドは base64 で JWT の payload を展開できますが、シークレットがなければ署名検証できません——誰でも「正しそうに見える」payload を偽造できます。
- 「この人は誰か、何かをしてよいか」というあらゆる判断は、必ず 拡張アプリバックエンドの
verifyWsaの結果を基準にしなければなりません。 - したがって標準的な戦略は次のとおりです:フロントエンドが
wsa/codeを拡張アプリバックエンドに渡す → バックエンドが検証 → バックエンドがセッションを確立 → フロントエンドはこのセッションだけを信頼する。
次のステップ:完全な API は SDK APIリファレンス を参照。問題が発生した場合は トラブルシューティング を参照してください。
