Skip to content
エンジニアリング

請求可能なタイムエントリをメールから1クリックで承認する方法 — そして SOC 2 監査も通す

JieGou の署名済みアクションリンクは、4分の SSO+2FA 承認儀式をメール内のワンクリックに置き換えました。本記事はワイヤフォーマット、検証順序、脅威モデル、そして監査人エビデンスと等価にする4つのプロパティです。

JT
JieGou Team
· · 3 分で読めます

あの4分の承認

前回の記事でこう書きました:「暗号学が SSO + 2FA がかつてしていた仕事をした。」何人かのエンジニアからコードで実際にどういう意味なのか質問が来ました。本記事はその答えです。

スタート地点は MSP 顧客ワークフローで測定可能な摩擦でした。AI が請求可能な ConnectWise タイムエントリを提案。マネージャーがポストする前にサインオフが必要。レガシーパスはこうでした:

  1. Slack 通知:「承認が必要」
  2. リンクをクリック → SSO リダイレクト → パスワード → 2FA → 承認ページ
  3. エントリを読む → 承認をクリック

最初のアクションまでの中央値:4分。エントリ30件/日 × 典型的な10席 MSP の4テクニシャンで、それは1日8時間のマネージャー注意力で、機能的には親指立ての山をやることに費やされる。

その4分は誰かが遅いからではなかった。承認への経路が、情報的には単一ビット(承認/却下)プラスオプショナルな理由テキストでしかないものに完全な ID 儀式を要求していたからでした。

私たちはそれをこう置き換えました:マネージャーのメールには承認却下のボタンが含まれます。1クリック。アクションが実行され、監査ログが記録し、タイムエントリが ConnectWise にポストされる。ログインなし。2FA なし。

SOC 2 質問票に通ります。以下がその仕組みです。

監査人エビデンスと等価にする4つのプロパティ

各ボタンのリンクは ?approval_id=42 付きの URL ではありません。サーバの秘密鍵で署名された JWT (JSON Web Token) で、4つのプロパティを持ちます:

  1. 署名済み。 トークンの真正性は暗号学的に検証可能。URL バーは信頼しない;署名を信頼する。
  2. 時間制限付き。 各トークンは期限切れになる(exp クレーム)、通常は発行から72時間後。それ以降はクリックしても何もしない。
  3. 単回使用。 トークンはちょうど1回だけ償還できる。同じ承認ボタンの2回目のクリックは冪等性キャッシュにヒットして「[タイムスタンプ]に既にあなたが承認済み」を返す。
  4. 冪等。 ネットワークが何をしようと — 重複クリック、リトライ、ブラウザのプリフェッチ — 基底アクションは最大1回しか実行されない。毎回同じ効果。

最初のプロパティが SSO をスキップさせる。他の3つが監査チームの「でももし…」質問をスキップさせる。

ワイヤフォーマット

トークンペイロード(jwt.decode 後だが署名検証前)はこうです:

{
  "iss": "jiegou.ai",
  "aud": "approval.action",
  "iat": 1714502400,
  "exp": 1714761600,
  "jti": "01HZ8B3X9NQK5W2T7P0V4M6Y8R",
  "approval_id": "appr_2bF7kQpL9xR3",
  "approver_email": "alex@example-msp.com",
  "action": "approve",
  "account_id": "acc_4nM8tYz2",
  "scope": "time_entry"
}

このペイロードについて3つ:

  • トークンはアクションを特定の承認者にバインドする。メールが転送されても、受信者は自分として償還できない — 監査ログは依然として alex@example-msp.com を承認者として記録する、転送者ではなく。(転送メール脅威は後述。)
  • jti は ULID — トークンごとに一意で、償還キャッシュの冪等性キーとして使われる。
  • scope フィールドはサーバにどの償還ハンドラへディスパッチするかを伝える。time_entrycontent_publishoutbound_message とは異なるコードパスを通る。同じ JWT パターン、異なる scope ハンドラ。

検証順序が大事

クリックがサーバに到達したら、私たちはこの正確な順序で検証します。任意のステップが失敗したら、止めます:

  1. 署名を検証する。 これが最初 — パース前、任意のフィールドを信頼する前。署名が無効ならリクエストは詳細なしの汎用 400 Bad Request を受け取る。どのフィールドが失敗したかは漏らさない。
  2. issaud をチェック。 間違った発行者または対象者 → 拒絶。これが他のサービスで鋳造されたトークンやシステム間でリプレイされたトークンを捕える。
  3. exp をチェック。 期限切れ → 「この承認リンクは期限切れです、新しいものをリクエストしてください」ページをレンダー。
  4. jti を償還キャッシュに対してチェック。 既に償還済み → 「[承認者]が[時刻]に既に承認済み」冪等性レスポンスをレンダー。
  5. 承認レコードをルックアップ。 approval_id が存在しないか、異なるパス(例:コンソールで承認)を通じて既に解決済みの場合、「この承認は既に解決済み」をレンダー。
  6. approver_email をその承認の許可された承認者リストに照合。 alex@... がこの承認のポリシーの承認者リストにいなければ、403 で拒絶。
  7. アクションを実行。 アトミックに:償還レコードを書く(jti → 償還時刻)、approval_event 監査エントリを発出、承認後ワークフローをディスパッチ(例:タイムエントリを ConnectWise にポスト)。
  8. 成功をレンダー。 確認ページを表示;トークンを再発行しない;再共有可能なものを URL バーに保存しない。

重要な明白でない選択はステップ1。パース前に検証。 一部の実装は最初に jwt.decode して kid (key ID) を抽出して署名検証するが、それは未署名データを既に信頼していることを意味する。1回の呼び出しで検証し、クレームを暴露する前に不正な形式のトークンを拒絶する JWT ライブラリを使う。

脅威モデル — 4つのこと、それぞれの攻撃者へのコスト

私たちは最も妥当な4つの攻撃を座って考えました:私たちの設計はそれぞれをどう扱うか?

1. メールが社外に転送される

何が起こる: alex@... 以外の誰かがボタンをクリックする。

攻撃者へのコスト: ゼロ — 平文でトークンを持っている。

緩和: トークンはアクションを監査ログ内の alex@... のアイデンティティにバインドする。転送者は自分として償還できない。アクションは実行される、しかしAlex として。次のガバナンスレビューで「Alex が IP 203.0.113.7 からこれを承認した(それは Alex のオフィスではない)」が浮上する — そして Alex は次回のログイン時にすぐに気付く。

これが暗号学的に完全には防げないものです。私たちは運用的に防ぎます:承認者 IP/UA の異常検知、週次ガバナンスサマリー、そして「あなたは alex@example-msp.com として承認しようとしています — 続けますか?」と言う3秒の確認ページ。メールリンクの転送詐欺は検知可能だが予防可能ではない。私たちは顧客 SOC 2 質問票でこれを明確に文書化します。

2. トークンがどこかのサーバログで漏れる

何が起こる: トークンが nginx アクセスログ、3rd-party Webhook ログ、迷子の console.log に現れる。

攻撃者へのコスト: 鮮度に依存。

緩和: 3層。

  • 短い exp 72時間期限切れは、漏れたトークンが3日以内に死ぬことを意味する。30日トークンは発行しない。
  • 単回使用償還。 正規のユーザーが先にクリックしたら、トークンは死ぬ。償還キャッシュルックアップは任意の副作用の前に発生する。
  • ロギング衛生。 アクションリンク URL はアクセスログと SIEM 取り込みでパターンマッチしてリダクトされる。トークンは JWT 自体以外の永続ストレージに到達することはない。

3. リプレイ — 同じトークンが2回クリックされる

何が起こる: ネットワークリトライ、ダブルクリック、ブラウザプリフェッチ、または悪意あるリプレイ。

攻撃者へのコスト: ゼロ(正規のリプレイは一般的)。

緩和: 検証ステップ4の償還キャッシュチェックが耐荷重。キャッシュは Redis、jti でキー化、TTL は exp より長い。最初の償還は原子的にキーをセット(SETNX);すべての後続クリックは既存のレコードを読み、誰がいつ承認したかを含む冪等な「既に承認済み」レスポンスを返す。

冪等性は単に UX の良さではない — システムがリトライ下で安全であり、状態を漏らさないようにするプロパティ。

4. 私たちの署名鍵が侵害される

何が起こる: HMAC シークレットまたは RSA 秘密鍵が漏れる。

攻撃者へのコスト: 任意の承認のために任意のトークンを鋳造できる。

緩和: 4層。

  • 鍵の隔離。 署名鍵は KMS にあり、アプリケーションメモリにはない。アプリケーションサーバは署名と検証のために KMS を呼ぶ;生の鍵素材を所有することはない。
  • 鍵のローテーション。 鍵は公開されたスケジュールでローテーションされる;古い鍵は最長発行 exp に等しい猶予ウィンドウの間のみ検証用に受け入れられる。
  • 承認者ごとにスコープされたトークン。 鍵侵害があっても、alex@... 用にトークンを鋳造してクリックする攻撃者は Alex のアイデンティティ下で監査エントリを生成する、それは(脅威1により)検知可能。
  • 侵害対応。 鍵侵害を検知したら、鍵を取り消し、すべての未処理トークンを無効化する。顧客は再発行まで「この承認リンクはセキュリティ上の理由で無効化されました;コンソールを使って解決してください」を見る。

これが完全には緩和しにくい脅威で、だからこそシステムにフォールバックがある:インコンソール承認キューは常に動作する。メールボタン承認は追加であって置き換えではない。

SSO + 2FA との誠実なトレードオフ

メールボタン承認は厳密には SSO + 2FA と等価ではない。トレードオフ:

プロパティSSO + 2FA署名済みアクションリンク
アイデンティティを認証はい(因子+因子)いいえ — 単一の決定をアイデンティティに紐付けて認証
メール転送に耐性はい(転送ユーザーは 2FA を通れない)検知可能だが予防できない
鍵侵害に耐性私たちのインフラから独立(連邦 IdP)私たちの KMS 衛生に依存
承認ごとの摩擦4分5秒未満
監査トレイル等価はいはい(同じ approval_event 形状)

私たちは顧客にどのトレードオフを選んでいるか明示します。推奨設定:

  • 低リスク・大量承認(タイムエントリ、コンテンツ草稿、カレンダーホールド):デフォルトでメールボタン
  • 高リスク承認(閾値超の金融取引、外部に見えるコミットメント、コンプライアンス影響アクション):コンソール+SSO 必須;メールボタンは「リマインダー」のみに
  • アカウントごとのオーバーライド:すべての顧客がどのアクションがメールボタン vs. コンソールのみを得るかの閾値を設定できる

閾値はコンフィグ値。暗号学は同じ。

監査ログ等価

すべての署名済みアクションリンク償還は、コンソール承認と同じ approval_event 形状を発出する。エントリーポイントフィールドは email_button と読む;残りは同じ:

{
  "event_type": "approval.resolved",
  "approval_id": "appr_2bF7kQpL9xR3",
  "approver_email": "alex@example-msp.com",
  "decision": "approve",
  "decision_at": "2026-04-29T14:32:18Z",
  "entry_point": "email_button",
  "client_ip": "203.0.113.42",
  "user_agent": "Mozilla/5.0 …",
  "jti": "01HZ8B3X9NQK5W2T7P0V4M6Y8R"
}

entry_point が監査人がメールから来たことを知る必要がある唯一のもの。他のすべて — 誰、いつ、何、IP — はコンソール承認エビデンスと一致する。私たちの SOC 2 エビデンスエクスポートフォーマットはそれらを同一に扱う。監査人は1つのスキーマを読む。

このパターンに合う他の5つのワークフロー

JWT-償還-キャッシュパターンが存在するようになったら、多くの単一決定ワークフローに合うことがわかった:

  1. カレンダーホールドの確認 — AI が「水曜午後2時に Q3 レビュー準備のために30分ブロック」を提案;オペレーターが確認または移動
  2. コンテンツ公開ホールド-アンド-リリース — 自動チェックを通ったが送信者確認のために保留マークされた草稿
  3. アウトバウンドメッセージホールド — AI が顧客返信を草稿;オペレーターがホールドキューからリリース
  4. センシティブな統合の解除 — AI が利用ごとの承認ゲートの後ろにある特定のツールを使う許可をリクエスト
  5. 単発同意フロー — 顧客のデータに影響するアクションのための顧客向け同意

これらはそれぞれかつてコンソール訪問が必要だった。それぞれ今やメールボタン等価がある。暗号学がクレデンシャル。 オペレーターの一日はその分縮む。

同じものを構築する別のチームに伝えたいこと

似たものを構築するなら、リーンインする3つと避ける3つ:

リーンイン:

  • 任意のフィールドパース前の JWT 署名検証。 1回の呼び出しでこれをするライブラリを使う。
  • jti でキー化された償還キャッシュ冪等性。 これがリトライセーフにする。
  • エントリポイントに関わらず同じ監査イベント形状。 監査人はクリックがどのパスから来たか気にしない。

避ける:

  • 「親切のために」長い exp 30日承認リンクは30日アタックサーフェス。72時間が人間の承認者には十分。
  • ブラウザがログしないことを期待してトークンを URL フラグメントに保存。 ブラウザはする。URL は「いずれログされる」と扱え。
  • サイレント失敗モード。 検証が失敗したら、明確なエラーページをレンダー。正規の承認者は週単位でメールを無視して古いトークンを持っているかもしれない。

前回の記事のスローガンはこうでした:信頼は弱まらなかった、摩擦の低いレイヤーに再配置された。 署名済みアクションリンクはまさにその文字通りの例。信頼は SSO + 2FA + パスワードマネージャー + ブラウザセッション から よく管理された署名鍵 + 短期 exp JWT + 冪等償還キャッシュ に移った。摩擦は4分から5秒未満に。監査トレイルはより多くのフィールドを得た、より少なくではなく。

無料で始める → — または MSP を経営していて上記の4分承認問題が馴染み深いなら、30分のウォークスルーを予約、初日からメールボタン承認でアカウントをセットアップします。

security approvals jwt operator-ux soc2 engineering
この記事をシェアする

この記事はお役に立ちましたか?

ワークフローのヒント、製品アップデート、自動化ガイドをメールでお届けします。

No spam. Unsubscribe anytime.