はじめに
別々のドメインで動いている複数のサイトで、1度のログインを共有したい。そう決めた瞬間に出てくるのが「では何を、どうやって渡すのか」という問題です。
移動先のサイトへ、パスワードを入力させずに「この人はログイン済みです」と伝える。その受け渡しに使う短い文字列が、ハンドオフトークンです。サイト間シングルサインオン(SSO)の中心にある部品ですね。
ただ、渡すものは途中で盗まれる・書き換えられる前提で設計しないといけません。ここを甘く作ると、便利にするための仕組みがそのまま他人になりすます入口になります。この記事では、トークンに何を入れ、どうやって偽造を防ぎ、なぜ数十秒しか使えない使い捨てにするのかを順に説明します。関数の書き方ではなく、判断の理由のほうです。
トークンに何を入れるか
中身は「誰が・どこへ・いつまで」
ハンドオフトークンは、それ自体が短い情報のかたまりです。最低限、次の5つを持たせます。
表が示しているのは、トークンが「引き継ぎに必要な最小限の情報」と「それが本物である証明」の2種類だけでできている、ということです。
前半の3つは受け取る側が「誰のログインを、どこで、いつまで有効なものとして扱うか」を判断するための材料。後半の2つは、その材料が信頼できるかどうかを機械的に確かめるための仕掛けです。役割がはっきり分かれているので、あとから項目を足すときにもどちら側の話なのかで判断できます。
パスワードのような秘密情報は載せない
大切なのは、トークンにパスワードや決済情報のような秘密を入れないことです。これはあくまで「このユーザーのログインを引き継いでよい」という一時的な許可証にとどめます。
理由は単純で、万一漏れたときの被害の大きさが変わるからです。パスワードが入っていれば、そこから本人になりすまして何でもできてしまう。一方、引き継ぎ専用の許可証であれば、漏れても「期限内に、そのサイトで、1回だけログイン状態を作れる」以上のことはできません。被害の上限をあらかじめ低く設計しておく、という考え方です。
情報を減らすこと自体が安全対策になる
トークンはURLに載って別サイトへ飛んでいきます。つまり、ブラウザの履歴やサーバーのアクセスログに文字列として残る可能性がある、ということです。
だからトークンに載せる情報は「残っても許容できるもの」だけに絞ります。ユーザー識別子も、社内で使っている会員番号そのものではなく、引き継ぎのためだけに用意した参照用の値にしておくと安心です。メールアドレスや氏名を入れないのも同じ理由。中身を最小にすることは、それだけで立派な対策になるんです。
署名で偽造を防ぐ
共有シークレットで署名を作る
トークンが途中で書き換えられていないことを保証するのが署名です。今回はHMAC署名(共通の秘密の鍵を使って、中身から短い検証用の文字列を計算する方式)を使いました。
サイトAが、共有している秘密の鍵とトークンの中身から署名を計算し、末尾に付ける
サイトBが、受け取った中身と同じ鍵で署名を計算し直す
自分で計算した署名と、付いてきた署名が一致すれば改ざんなしと判断する
図が示しているのは、受け取った側は同じ計算をもう一度やるだけで真偽を判定できる、ということです。
鍵を知らない第三者は、中身を書き換えても正しい署名を作れません。だから「ユーザー識別子だけ別人にすり替える」といった細工が成立しない。逆に言えば、鍵さえ守れていれば、トークンがどこを経由してきたかを気にしなくて済みます。
署名は暗号化ではない
署名と暗号化は目的が違う
HMAC署名はトークンを暗号化するものではありません。中身は読める状態のまま、「書き換えられていないこと」だけを保証します。中身を秘密にしたいのではなく、偽造を防ぎたいのが目的なので、これで足ります。
ここを取り違えると設計を誤ります。署名があるからといって中身が隠れているわけではないので、前の見出しで書いたとおり「読まれても困らない情報だけを載せる」という前提はそのまま必要です。署名は中身を隠す道具ではなく、中身の真偽を判定する道具、という整理ですね。
公開鍵方式ではなく共有シークレットを選んだ理由
署名の方式には、鍵を発行側と検証側で分ける公開鍵方式もあります。相手が外部の会社で、鍵を渡したくない場合はそちらが向いています。
今回はすべてのサイトが同じ運営主体の管理下にあり、同じ秘密の鍵を安全に配れる前提がありました。であれば、仕組みは単純なほうが運用のミスも減ります。鍵の種類が1つで済むぶん、設定漏れや取り違えも起きにくい。将来的に外部パートナーのサイトを仲間に入れるなら方式を見直す、という線引きだけ決めて進めました。
有効期限と使い捨てで再利用を防ぐ
寿命は移動に必要な数十秒だけ
署名で偽造を防げても、正規のトークンそのものが盗まれる可能性は残ります。そこで効いてくるのが有効期限の短さです。
ハンドオフトークンは、サイトを移動する数秒から十数秒のためだけに存在します。だから寿命も極端に短く設定しました。仮に履歴やログから拾われても、使おうとした頃にはとっくに期限切れ、という状態を作るわけです。長く使えるトークンは、存在しているだけで狙われる対象になります。短さは不便さではなく、設計上の意図です。
1度使ったトークンはその場で無効にする
盗まれたトークンを、期限内なら何度でも使える。同じ文字列で繰り返しログイン状態を作られてしまう
1度検証に通ったトークンはその場で無効化。同じトークンの2回目は必ず失敗する
図が示しているのは、同じトークンを2回使えるかどうか、という一点の違いです。
受け取った側は、検証に成功したトークンのワンタイムIDを記録しておき、同じIDが再び届いたら拒否します。これで、盗んだトークンを使い回すリプレイ攻撃を封じられます。期限内であっても2回目は通らないので、有効期限だけに頼るより確実です。
3つの対策がそれぞれ別の攻撃に対応する
署名・有効期限・使い捨ては、目的が重なっていません。署名は中身の書き換えを、有効期限は時間差での悪用を、使い捨ては同じトークンの使い回しを、それぞれ止めています。
だから3つはどれも省けません。署名だけなら盗んだ本物をそのまま使われるし、有効期限だけなら中身をすり替えられる。1つが破られても他が残る状態を作っておく、という重ね方をしています。認証まわりは仕様の抜けがそのまま事故になるので、対策ごとに「これが無いと何ができてしまうか」を書き出してから設計を固めました。
発行と保管で決めておいたこと
発行できる相手を絞る
トークンを作る窓口は、誰でも叩ける状態にしてはいけません。発行を受け付けるのは、そのサイトに既にログイン済みのユーザーからの要求だけ、という条件を最初に付けます。
ここが緩いと、ログインしていない誰かが「このユーザーの分をください」と頼んで許可証を手に入れられてしまう。トークンは既に済んでいる認証の結果を持ち運ぶためのものなので、認証されていない相手に発行する場面はそもそも存在しない、という整理です。
宛先サイトを書き込んで使い回しを防ぐ
トークンには「どのサイト向けに発行したか」を書き込み、受け取った側は自分宛かどうかを確認します。
これがないと、サイトB向けに出したトークンをそのままサイトCへ持ち込めてしまいます。同じ鍵を全サイトで共有している構成では、署名だけでは宛先を区別できないためです。宛先を書き込んで照合する、という一手間で、サイトをまたいだ使い回しを止められる。地味ですが、共有鍵方式を選んだからこそ外せない確認です。
シークレットの置き場所と入れ替え
署名に使う鍵は、コードには一切書かず、環境変数(サーバーごとに外から渡す設定値)として持たせています。コードに書いてしまうと、リポジトリを見られた時点で全サイトの署名を偽造できてしまうためです。
あわせて決めたのが、鍵を入れ替えるときの手順です。全サイトが同じ値を参照している以上、入れ替えは全サイト同時に行う必要があります。頻繁に変えるものではありませんが、担当者が代わったときなどに備えて、手順だけは文書に残しました。
まとめ
ハンドオフトークンは、サイトをまたぐ一瞬だけ有効な使い捨ての許可証です。設計の要点を振り返ります。
- 中身は最小限:秘密情報は載せず、引き継ぎに必要な情報だけにする
- 署名で偽造を防ぐ:共有した鍵で計算し、受け取った側が同じ計算で照合する
- 寿命は数十秒:盗まれても使う前に期限が切れる長さにする
- 1回きり:使用済みのIDを記録し、2回目を必ず拒否する
- 発行と宛先も絞る:ログイン済みの要求だけに発行し、宛先サイトを照合する
このトークンを受け取った側が、実際にどう検証してログイン状態を作るのかは「SSOコールバックとセッション確立のフロー」で解説します。SSO全体の位置づけは「複数ECサイト間のシングルサインオン設計」を、鍵や許可サイトの管理方法は「サイト追加に耐えるSSO設定管理」をご覧ください。