Frontend Craft Labフロントエンド開発の実験場
← 記事一覧に戻る
security

reCAPTCHA v3 の検証応答で createTime が1970年となる原因と対策

現象と背景

reCAPTCHA v3(※特にreCAPTCHA Enterpriseのスコア方式)をサーバ側で検証した際、 レスポンス中のTokenProperties.createTime フィールドが時折 1970-01-01T00:00:00Z (Unixエポック)となるケースが 報告されています。

このタイムスタンプはデフォルト値であり、本来そのトークンの生成日時が取得できなかったことを意味します。 通常、正常なトークンであれば例えば "2024-12-03T04:52:23.582Z" のように実際の発行日時が記録されます。 したがって、 1970-01-01T00:00:00Z と表示されるのはトークンが無効(不正または無効化)と判断された場合に起こる挙動だと考えられます。

以下では、なぜトークンが無効と判断され createTime がエポック時刻になってしまうのか、考えられる原因を調査し、関連する報告例や公式ドキュメントの情報をまとめます。 また、そのような事態を避けるための実装上の注意点や対策についても説明します。

createTime が1970年になる主な原因

トークンが無効と見なされる状況にはいくつかのパターンがあり、それぞれで検証結果の tokenProperties.valid がfalseとなり、 invalidReason (無効理由)のコードが付与されます。 こうした場合に有効な生成時刻が得られないため、 createTime がデフォルト値(Unixエポック)になってしまいます。

主な原因とその報告例・考えられる要因は次のとおりです。

  • トークンの形式不正(MALFORMED)invalidReason: MALFORMED

提供されたトークン文字列の形式や内容が正しく解析できない場合、Googleは invalidReason: MALFORMED としてトークンを無効と判断します。 この状況ではトークンの生成タイムスタンプを取得できないため、 createTime は 1970-01-01T00:00:00Z と表示されます。 実際の例として、Qiitaの記事で無効(NG)レスポンスにおいて "valid": false かつ "invalidReason": "MALFORMED" となり、 createTime がエポック時刻になっていることが示されています。

MALFORMEDの原因としては「トークンの一部が欠損・改変されている」「想定外のフォーマットになっている」ことが考えられます。 例えば、クライアントからサーバーへトークンを送る際にトークン文字列が途中で切れて送信されてしまった場合などです。 GitHub上でも、iOSアプリ内で取得したreCAPTCHAトークンをバックエンドに送る際に一部が欠落し、無効なトークンとなっていた事例があります(GETパラメータでトークンを送るとURLエンコードの問題で一部が失われる可能性が指摘されています)。 その場合もAPIの応答では valid=false ・ invalidReason:MALFORMED ・ createTimeエポックになると報告されています。

設定ミスによる形式不正のケースもあります。例えば、reCAPTCHA Enterpriseのキー作成時に「WAFアクション・トークン」を有効にしたにも関わらず通常の検証フローで評価しようとすると、取得したトークンは本来HTTPヘッダ経由で使われる特殊な形式のため「MALFORMED」と判断されてしまいます。 実際、ある開発者はEnterpriseキーでWAFアクションを有効にしたまま通常の検証を行ったところ常にMALFORMEDとなり、キーを作り直してその機能を無効化したら解決したと報告しています。 このようにキーの種類や取得方法に対する誤った実装もトークン形式不正につながります。

  • トークンの有効期限切れ(EXPIRED)invalidReason: EXPIRED

reCAPTCHAのトークンには約2分間という有効期限があります。そのため、発行後すぐに検証しなかった場合やユーザーがフォーム上で長時間操作していた場合、トークン送信時には期限切れとなって無効扱いされることがあります。Enterprise版の公式ドキュメントにも「各トークンは一度しか使えず、発行から2分後に期限切れとなる」と明記されています。

期限切れトークンに対してサーバ側で検証リクエストを送る と、 invalidReason: EXPIREDが返されます (この場合トークン自体は一度は正しく生成されているため、 createTime には本来の生成時刻が入ると考えられますが、結果的に無効なため処理としては失敗となります)。

頻繁に期限切れが発生する場合は、トークンを取得するタイミングを見直す必要があります(対策は後述)。

  • トークンの重複使用(DUPE)invalidReason: DUPE

reCAPTCHAのトークンは一度きりの使い捨てであり、同じトークンを2回以上検証に使うことはできません 。 1回目の検証では有効でも、同じトークンを再送すると2回目以降は無効となり、 invalidReason: DUPE (Duplicate)が返されます。 公式にも「一度評価されたトークンを再度送ると無効(DUPE)になる」旨が述べられています。 このケースでは通常、最初の使用時に既に生成時刻が記録されているため、 createTime はおそらく最初の発行時刻のままと推測されます(1970年にはなりません)。
しかし現象としては「初回は成功するが2回目は必ず失敗する」という形で現れます。 もし検証APIを二重に呼び出していたり、フォーム再送信時に前回のトークンを使い回していると、このDUPEエラーが発生します。 実際、ある開発者はフレームワーク上でログイン処理が二重実行され同じトークンを送っていたために常に invalidReason: DUPE となっていたことを報告しています。

  • サイトキーやドメインの不一致(SITE_MISMATCH)invalidReason: SITE_MISMATCH

トークンが発行されたサイトキーと、サーバ側で検証に使用しているサイトキーもしくはドメインが食い違っている場合も無効と判定されます。 例えば「異なるサイトのキーで取得したトークンを誤って検証に使っている」「reCAPTCHAの管理コンソールに現在のドメインが登録されていない」等の設定ミスです。

Googleのリファレンスにも、サイトキー不一致(SITE_MISMATCH)は「構成エラー(例: 開発用キーを本番で使用)や、他サイトのトークンを使おうとした場合」に発生すると記載されています。 このケースではTokenProperties.valid がfalseになりますが、 invalidReason に特定の値が入らない場合もあります。 (従来は SITE_MISMATCH コードがありましたが非推奨扱いです)。 代わりに invalidReason がUNKNOWN_INVALID_REASON となったり、Enterpriseキーではサイト名が一致しないと検証自体が失敗する可能性もあります。 Stack Overflowでも、テスト中にドメイン認証を無効化していたため別ネットワーク環境でのみエラー(おそらくブラウザエラー)が出ていたが、 正しくドメインをキーに登録したところ解決したとの報告があります(ドメイン未登録が原因で検証失敗していた例)。 このようにサイトキー・ドメイン設定の不整合もトークン無効化の一因です。

  • トークン未取得・未送信(MISSING)

検証リクエスト自体にユーザーのトークンが含まれていない場合、当然ながら検証はできません。APIレスポンスでは invalidReason: MISSING (トークン未提供)とされます。

例えばフロントエンドの実装ミスで、トークンをフォームに埋め込む処理が動かず空のまま送信してしまった場合や、 ユーザーがページを再読み込みして古いトークンを保持した状態で送ったために無効になった場合などが考えられます。 MISSINGの場合もトークンのタイムスタンプで送ったために無効になった場合などが考えられます。

MISSINGの場合もトークンのタイムスタンプ情報が無いため、 createTime はエポック時刻に見える可能性があります(あるいはフィールド自体が空/省略となることもあります)。 これは実装上の不備で比較的わかりやすいので、発生していないか確認すべきポイントです。

  • ブラウザ側エラー(BROWSER_ERROR)

ユーザーのブラウザでreCAPTCHAを実行する際にネットワーク障害やスクリプトエラーが起きた場合、トークンが正常に生成されないことがあります。 そのような場合、Enterprise APIでは invalidReason: BROWSER_ERROR (ブラウザエラー)が返されることがあります。 これは「クライアント側で再試行可能なエラーが発生した」ことを意味し、悪意のないユーザーでも環境によっては稀に起こりえます。 例えば特定のネットワーク(モバイル回線など)でreCAPTCHAがうまく通信できず、このエラーが発生した事例があります。

BROWSER_ERRORとなったトークンも有効な生成時刻を持たない可能性が高く、 createTime が1970年表示になる可能性があります。 頻繁に発生する場合はユーザーの環境依存の問題の可能性がありますが、基本的にはまずいやすい一時的エラーです。

トークンが無効というレスポンスからさらにその原因を特定する

以上のように、トークンが無効になる原因として MALFORMED(形式不正)・EXPIRED(期限切れ)・DUPE(再利用)・SITE_MISMATCH(サイト不一致)・MISSING(未提供)・BROWSER_ERROR(クライアントエラー) などが考えられます。 このうちトークン形式不正や未提供の場合には createTime がUnixエポックになる現象が確認されています。 EXPIREDやDUPEでは生成時間自体は取得可能なため通常は実際の時刻が入りますが、結果的に検証は失敗となります。

いずれにせよ、原因に応じてGoogleが invalidReason にコードを返しているため、まずはそれを手掛かりにどのケースかを特定することが重要です。 エポックになる現象が確認されています。EXPIREDやDUPEでは生成時間自体は取得可能なため通常は実際の時刻が入りますが、結果的に検証は失敗となります。

既知の報告や公式情報

上述した挙動について、いくつか参考になる外部の報告例や公式コメントがあります。

  • Qiitaでの実装報告

Qiitaの記事「PHPでGoogle reCAPTCHA Enterpriseを実装してみる」では、正常な場合とエラーの場合のAPIレスポンス例が公開されており、 エラー時に "invalidReason": "MALFORMED" および "createTime": "1970-01-01T00:00:00Z" となっていることが示されています。 この例は、本回答で説明した形式不正トークン時の挙動を裏付けるものです。

  • GitHubのIssue

GoogleCloudPlatformのreCAPTCHA EnterpriseモバイルSDKに関するIssueでは、iOS上で取得したトークンがサーバに送られる際に一部が欠落して無効(MALFORMED)になる問題が報告されています。 その中でGoogleの開発者から「トークンが途中で切れていないか?GETパラメータではなくPOSTボディで送らないと切れることがある」という旨の指摘がありました(長いトークンをURL経由で送信すると切断される可能性)。これは、実装上の不備(送信方法の問題)がトークン無効を招いた具体例と言えます。

  • Google公式ドキュメント

reCAPTCHA Enterpriseの公式サイトには、トークン検証時のTokenProperties 構造や invalidReason の種類について詳細な説明があります。 例えば「プロジェクトのアセスメント作成API」リファレンスには、 invalidReason に取りうる値として上記のMALFORMED, EXPIRED, DUPE, MISSING, BROWSER_ERRORなどが列挙されています。 また各コードの意味も「MALFORMED: 提供されたユーザートークンが不正」「EXPIRED: トークンの有効期限切れ」「DUPE: 既に使用されたトークン」「MISSING: トークンが存在しない」「BROWSER_ERROR:ブラウザでネットワーク障害等が発生」と説明されています。 これら公式情報からも、トークンが無効となる様々な要因が定義されていることが確認できます。

  • Stack OverflowのQA

reCAPTCHA Enterpriseに関するQ&Aでも、類似の問題や原因についての言及があります。 例えば、「invalid_reason: DUPE」という質問では同じトークンを2回検証すると2回目にDUPEになる理由が解説されています。 また別の質問では、Enterpriseで invalid_reason: 6 (当時ライブラリ未定義のコード)というエラーが頻発し、原因はサイトのドメイン未登録によるものだったと回答されています(ドメインをコンソールに追加して解決)。

このようにコミュニティでも設定ミスや実装ミスが原因となるケースが報告されており、対処法が共有されています。

回避策と実装上のベストプラクティス

TokenProperties.createTime が1970年になるような事態を防ぎ、また発生した場合に適切に対処するために、以下のようなポイントに留意してください。

  • フロントエンドでのトークン取得タイミング: トークンの有効期限は2分と短いため、ページ読み込み時にトークンを発行して放置するのではなく、 ユーザーのアクション直前に grecaptcha.execute() を呼び出して最新のトークンを取得するようにしましょう。 例えばフォーム送信の直前やボタンクリック時にトークンを発行する実装にすると、期限切れによる無効化(EXPIRED)を防げます。 また、ユーザーがフォーム入力に時間をかける可能性がある場合は、送信前にトークンを再取得(リフレッシュ)する仕組みを入れることも検討してください。

  • トークンの再利用禁止: 一度検証に使用したトークンは再度使わないように設計しましょう 。 例えば、フォームのバリデーションエラーで再送信が必要になった場合でも、必ず新しいトークンを発行してから送信する必要があります。 ページをリロードしないAJAX再送信の実装をする場合は、サーバからエラー応答を受け取った際にクライアント側で grecaptcha.enterprise.execute() を再度呼び出 し、新トークンを付与してから再リクエストを送るようにします。再利用防止の対策により、DUPEエラーを回避できます。

  • トークン送信方法の適切化: クライアントからサーバへのトークン送信にはHTTP POSTメソッドを使用し、トークンをリクエストボディまたはヘッダに載せるのが望ましいです。 URLのクエリパラメータ(GET)でトークンを送ると、URL長制限やエンコードの問題でトークンが途中で欠落する可能性があります。 実際にそれが原因でMALFORMEDになった例もあります。トークン文字列は長いため、きちんとエンコードされ完全な形でサーバに渡っているか確認してください。 フォームの場合は非表示フィールドにトークンをセットしてPOST送信、AJAXの場合はJSONボディに含める等、確実に全文字列が届く方法を取ります。

  • サイトキーとドメインの設定確認: 使用しているサイトキーが正しく設定されているか、開発・本番環境でキーの混同がないか確認しましょう。 特にEnterprise版ではキーに許可するドメインやアプリを登録できますが、これを正しく行わないと検証時に無効扱いになります。 テスト環境ではドメイン制限をオフにしていた場合も、本番移行時にはドメインを登録するのを忘れないようにします。 また、サイトキーの種別(v3スコア vs v2チェックボックス)も合っているか確認してください。 例えばv3のキーを持っているのにフロントでチェックボックスを表示しようとするとエラーになりますし、その逆(チェックボックスタイプキーでv3のexecuteを呼ぶ)も誤りです。 サイトキー不一致は見えづらいミスなので、念のため設定を再点検することをおすすめします。

  • 特殊な機能の利用有無: reCAPTCHA Enterprise固有の機能(WAF用アクショントークンやセッショントークン)を利用している場合は、その使用方法に従ってください。 WAFモードを有効にしたキーでは本来、トークンを通常のフォームフィールドではなくリクエストヘッダ( X-Recaptcha-Token など)で送信し、 かつサーバ側でも特定の検証手順を踏む必要があります。もしそうしたモードを使用していないのであれば、 キー作成時に誤って有効化していないか確認し、無用であれば無効化する方が無難です。 機能を誤用するとMALFORMEDエラーにつながります。

  • サーバ側での検証結果チェックとログ: サーバで検証APIの応答を受け取ったら、必ずtokenProperties.valid の真偽をチェックし、 falseであれば処理を中断してエラー対応を行います。その際、 invalidReason の内容をログに記録することで、 どの原因で無効になったかを把握できます。 例えば、invalidReasonが EXPIRED であれば「ユーザーが長時間放置した可能性」が考えられますし、 MALFORMED であれば「トークン送信不備のバグ」を疑うべきです。 DUPE なら「二重送信」、MISSING なら「トークン未取得」、 BROWSER_ERROR なら「ユーザー環境の一時的不具合」の可能性が高まります。 このように原因に応じたログを残すことで、後から問題の傾向を分析したり、ユーザーへのフィードバック (例えば「時間切れです、もう一度試してください」とメッセージを出す等)を適切に行えます。

  • エラー発生時のユーザー対応: トークンが無効で検証に失敗した場合の扱いも検討しておきます。 基本的にはフォーム送信を失敗扱いにし、ユーザーには再度試行してもらう必要があります。 特にEXPIREDやBROWSER_ERRORの場合、ユーザーには非悪意の場合もあるので「時間が経ちすぎたため、もう一度送信してください」と案内し、 再送時には新しいトークンを付与するようにします。MALFORMEDが頻発する場合は裏で自動再試行するよりも、 根本原因(実装バグ)を修正すべきです。いずれにせよcreateTimeが1970年になっているようなケースの多くは、 ユーザーには再チャレンジしてもらう以外にないため、一度失敗したらトークンを再取得してリトライ可能なUIにしておくと親切です。

以上の対策を講じることで、 TokenProperties.createTime が1970年(Unixエポック)となるケースを事前に防止し、 万一発生しても原因を特定して適切に対処できるようになります。特にトークンの取り扱い(取得タイミング・送信方法・使い回し禁止)とキーの設定確認が重要なポイントです。 適切な実装と監視によって、このような異常値が記録される事態を減らし、reCAPTCHA v3を安全に活用できるでしょう。

参考資料: Google reCAPTCHA Enterprise ドキュメント、APIリファレンス、 Qiita記事の実装例、StackOverflowおよびGitHubでのQ&Aなど。

これらは本回答内容の根拠となる情報源ですので、詳細は該当箇所の引用元もあわせてご参照ください。

Create assessments for websites | reCAPTCHA Enterprise | Google Cloud https://cloud.google.com/recaptcha/docs/create-assessment-website