はじめに
社内向けに、Excel(VBA)をフロントエンドにした顧客管理ツールを作っています。セキュリティの観点からサーバを立てるWebアプリは禁止。という付則付きの難題です。下記画像がHTMLで描写した操作画面になります。
構成はこんな感じです。
- 各利用者のPCに配る マクロブック(.xlsm) … 画面と業務ロジック
- 共有の データベース … 実データの置き場
このデータベース部分を、最初は「共有フォルダに置いたExcelブック」で作り(第1段階)、将来的に SharePointリスト へ移行する(第2段階)、という二段構えの計画にしていました。設定ファイルの1行を書き換えるだけでデータ層が差し替わるよう、インターフェースを切って抽象化してあります。
[利用者PC]
マクロブック
└ 画面(WebView2 + HTML)
│
ブリッジ層(VBA)
│
IDataStore(インターフェース)
├ ExcelStore … 第1段階:共有フォルダのExcelブック
└ SpStore … 第2段階:SharePointリスト
▲
設定ファイルの Mode= で切替
で、第2段階の設計を詰める段になって、前提が根本から崩れました。認証問題です。初期の想定ではオンプレSharePoint運用だったのですが、クラウドと判明し、てんやわんやです。
この記事は、その「崩れ方」と「どう組み直したか」の記録です。同じようにVBAから業務システムをクラウドにつなごうとしている人の役に立てば幸いです。
前提が崩れた瞬間
前述しましたが、設計書には、こう書いてありました。
HTTP:
WinHttp.WinHttpRequest.5.1、SetAutoLogonPolicy 0(Windows統合認証)
つまり オンプレミス版のSharePoint + Windows統合認証 を前提にしていたわけです。
VBAからオンプレのSharePointを叩くなら、これは非常に楽な方法です。社内ネットワークに参加しているPCなら、ログイン中のWindowsユーザーの資格情報が自動でサーバーに渡るので、コード上にIDもパスワードも一切書かなくていい。
' オンプレ + Windows統合認証なら、これで通っていた
Set http = CreateObject("WinHttp.WinHttpRequest.5.1")
http.SetAutoLogonPolicy 0 ' ← ログイン中のユーザーで自動認証
http.Open "GET", url, False
http.Send
ところが、情シスに確認したところ返ってきた答えが、
- SharePointは クラウド版(SharePoint Online)
- 認証は Entra ID(旧Azure AD)
- おそらく MFA(多要素認証)も有効
……はい、SetAutoLogonPolicy は使えません。
(返答もう少し早くほしかったなー・・・。)
なぜ使えないのか
Windows統合認証(NTLM/Kerberos)は、社内ネットワーク内でユーザーの身元を保証する仕組みです。「社内LANにいる=身元確認済み」という前提が成り立つ閉じた世界だから成立します。
クラウド版のSharePointは、当然ながら社内LANの外にいます。マイクロソフトのデータセンターにあるサーバーが「あなたの会社のADに聞いてみますね」とはならない。代わりに、インターネット越しにEntra IDへ問い合わせて「通行手形(アクセストークン)」をもらい、それをリクエストに添えて送るという、OAuth 2.0 の世界に移ります。
この「①②をどうやるか」が、今回の難所でした。
MFAという壁
VBAからOAuth 2.0でトークンを取る方法は、大きく3つあります。
しかし今回は MFAが有効かもしれない という条件があります。これが効いてきます。
MFAは、ID・パスワードに加えて「スマホの認証アプリで承認」「SMSの確認コードを入力」といった追加の確認を挟む仕組みです。人間がブラウザでログインする分には何の問題もありません。
問題は、今回アクセスするのが「人間」ではなく「マクロ」だということです。
マクロが裏でデータを読み書きするたびに、スマホに通知が飛んで承認しないと進まない——業務ツールとしては成立しません。
選択肢の整理
| 方式 | 概要 | MFA有効時 |
|---|---|---|
|
ROPC (Resource Owner Password Credentials) |
ユーザー名とパスワードを直接投げてトークンを取る | ❌ 基本ブロックされる |
|
アプリのみ認証 (Client Credentials) |
プログラム自体をアプリとして登録し、クライアントシークレットや証明書でトークンを取る | ⭕️ 影響を受けない |
|
デバイスコードフロー (Device Authorization Grant) |
画面にコードを表示し、利用者が別途ブラウザでログインする | ⭕️ MFAも普通に通せる |
一番シンプルなのはROPCです。設定ファイルにIDとパスワードを書いておけば自動で通る。でも、これは MFAが有効なテナントではまず通りません。そもそもマイクロソフトも非推奨としている方式なので、ここは早々に捨てました。
というわけで、実質「アプリのみ認証」か「デバイスコードフロー」の二択になります。
案1:アプリのみ認証 —— そして気づいた落とし穴
最初に有力だと思ったのは アプリのみ認証(Client Credentials) でした。
これは「〇〇さんとしてログインする」のではなく、プログラムそのものを1人の利用者としてEntra IDに登録する方式です。人間ではないので、当然MFAは関係ありません。固定の「合言葉」(クライアントシークレット)や証明書を提示すれば、いつでもトークンがもらえます。
シンプルで、利用者に一切の操作を求めません。理想的に見えました。
で、落とし穴
このクライアントシークレットを、どこに置くのか?
配布するのはマクロブックです。営業部の全員のPCにコピーされます。つまり、
共有の合言葉が、全利用者のPCにばらまかれる
ということになります。
これが厄介なのは、単に「秘密が漏れる」だけではありません。このツールには、担当者ごとの権限管理や、同時編集を防ぐロック機能が実装されています。それらは全部ツール側のロジックで担保しているものです。
シークレットが漏れると、そのツールのロジックを一切通さずに、直接SharePointのAPIを叩けてしまう。権限チェックもロックも素通りです。極端な話、PowerShellの数行で全顧客データを抜くことも、書き換えることもできてしまいます。
「マクロブックの中に隠しておけばバレないのでは?」と一瞬考えましたが、VBAのコードなんてパスワード保護をかけたところで解析されます。設定ファイルに書けばなおさら平文です。
「ハッシュ化すればいいのでは?」の誤解
ここで一度「シークレットをハッシュ化して保存すれば?」と考えました。が、これは成立しません。
ハッシュ化は「元に戻せない一方向の変換」です。パスワード保管でよく使われますが、あれが成立するのは、サーバー側が元のパスワードを持つ必要がないからです。届いたパスワードをハッシュ化して、保管済みのハッシュと一致するか比べるだけでいい。
一方、今回のクライアントシークレットは、Entra IDに対して「本物そのもの」を提示する必要がある。ハッシュ化して別物にしてしまったら、Entra IDは「知らない値だ」と突っぱねます。
ハッシュ化が使えるのは「照合するだけの秘密」。
使うたびに本物を提示する必要がある秘密には使えない。
この区別、意外と混同しがちなので書き留めておきます。
一応の緩和策
アプリのみ認証を採用するなら、以下の積み重ねでリスクを下げることになります。
| 対策 | 効果 |
|---|---|
| 証明書認証にする | 秘密鍵をOSの証明書ストアに置ける。平文の文字列よりは持ち出しにくい |
権限を最小化(Sites.Selected 等) |
漏れても被害範囲が特定サイトだけに限定される |
| DPAPI等でPCに紐付けて暗号化 | ファイルを他PCにコピーしても復号できない |
| シークレットに短い有効期限を設定 | 漏洩が放置される期間を短縮する |
| 条件付きアクセスでIP制限 | 社外から使われても弾ける |
| サインインログの監視 | 不審なアクセスの検知 |
……対策は打てます。打てますが、「共有の秘密が存在する」という構造そのものは消えません。全部「漏れた後の被害を小さくする」話であって、根本原因の除去ではない。
やや気持ち悪さが残りました。
案2:委任認証 × デバイスコードフロー
そこで出てきたのが、もう一つの案です。
ツール自身に共通の合言葉を持たせるのをやめて、利用者本人にログインしてもらう。
これを 委任認証 と言います。利用者が自分のEntra IDアカウントでログインし、「このアプリが私の代わりにSharePointへアクセスすることを許可します」という形でトークンが発行される。以降、そのトークンは その人の権限で 動きます。
つまり、アプリ側に埋め込む固定の秘密が存在しない。漏れるものがそもそもない。
でも、VBAでどうやってログイン画面を出すの?
ここが最初の疑問でした。
一般的なWebアプリのOAuthは、「ブラウザをリダイレクトさせて、認証後に指定のURLへコールバックを受ける」という流れです。これをやるにはコールバックを受け取るローカルサーバーが必要になります。
ところが本ツールの設計方針には、はっきりこう書いてありました。
サーバープロセスは存在しない(ローカルファイル + WebView2 で動作、http 不要)
サーバーを立てる時点で、配布・運用の前提が崩れます。ポートの競合、ファイアウォールの許可、常駐プロセスの管理……業務ツールとしては重すぎる。
ここで効いてくるのが「デバイスコードフロー」です。
デバイスコードフローとは
もともとは、テレビやゲーム機のように「キーボードがない・ブラウザが使えない機器」のために作られた認証方式です。動画配信アプリの初期設定で「テレビ画面にコードが出るので、スマホでこのURLを開いて入力してください」というアレです。
これがそのままVBAに使えます。
ポイントは ⑥のポーリング です。
マクロは「利用者がログインし終わったか?」を、Entra IDに数秒おきに自分から聞きに行きます。つまり通信は全部 マクロ → 外向き のみ。外部からのコールバックを受ける必要がないので、ローカルサーバーが一切要りません。
そして⑤で普通にブラウザが開くので、MFAもそのまま通ります。むしろMFAと相性が良い。
VBAの実装イメージはこんな感じになります(※現時点では設計段階のスケッチです)。
' ① デバイスコードを要求
Set http = CreateObject("WinHttp.WinHttpRequest.5.1")
http.Open "POST", "https://login.microsoftonline.com/{tenant}/oauth2/v2.0/devicecode", False
http.SetRequestHeader "Content-Type", "application/x-www-form-urlencoded"
http.Send "client_id=" & CLIENT_ID & "&scope=" & SCOPE
' ② 返ってきた user_code / verification_uri を画面に表示
' → 利用者がブラウザでログイン
' ⑥ ポーリング(authorization_pending が返る間は待つ)
Do
http.Open "POST", TOKEN_ENDPOINT, False
http.Send "grant_type=urn:ietf:params:oauth:grant-type:device_code" & _
"&client_id=" & CLIENT_ID & "&device_code=" & deviceCode
' → access_token が返るまでリトライ
Loop
なぜシークレットが不要になるのか
そしてここが、個人的にいちばん腑に落ちた部分です。
デバイスコードフローでは、クライアントシークレットを使いません。 案1で頭を抱えていた「全PCに秘密をばらまく問題」が、方式を変えただけで消滅しました。
なぜそうなるのか。OAuth 2.0では、アクセスしてくるプログラムを2種類に分類しています。
| 種類 | 意味 | 例 |
|---|---|---|
|
機密クライアント (Confidential Client) |
秘密を安全に保管できる環境で動く | サーバー上のWebアプリ、バッチ処理 |
|
パブリッククライアント (Public Client) |
秘密を安全に保管できない環境で動く | スマホアプリ、ブラウザ上のJS、デスクトップアプリ |
分類の基準は「プログラムの実体が利用者の手元に渡るかどうか」です。
サーバー上で動くならコードもファイルも管理者しか触れないので、秘密を置いておけます。でも各PCに配布するマクロブックは、ファイルそのものが利用者の手に渡る。中身は解析できるしコピーもできる。何を隠しても隠しきれないのが前提です。
つまり——
配布するマクロブックは、構造上どうやってもパブリッククライアントである。
案1で悩んでいたのは、要するに「パブリッククライアントなのに、機密クライアント向けの方式を使おうとしていた」からでした。無理が出るのは当然です。
デバイスコードフローは、最初からパブリッククライアント専用に設計されています。「どうせ秘密を守れない場所で動くのだから、シークレットを使わない仕組みにしよう」という発想です。Entra IDのアプリ登録にも「パブリック クライアント フローを許可する」というスイッチがあり、これを有効にすればシークレットなしでトークンを取得できます。
実際、トークン要求時に送るのはこれだけです。
grant_type=urn:ietf:params:oauth:grant-type:device_code
client_id=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx ← 秘密ではない
device_code=xxxxx ← その場限りの使い捨て
client_secret がどこにもありません。
「秘密がないのに、なぜ安全なのか」
当然の疑問だと思います。ここが一番大事なところです。
答えは、身元を証明しているのがプログラムではなく、利用者本人だからです。
| アプリのみ認証 | デバイスコードフロー | |
|---|---|---|
| 「誰であるか」を証明するもの | クライアントシークレット | 利用者本人のログイン(ID/パスワード + MFA) |
| シークレットが漏れたら | 即アウト(誰でもデータを取れる) | そもそも存在しない |
アプリのみ認証では、シークレットが唯一の身分証でした。だから漏れた瞬間に破られる。
デバイスコードフローで身分証の役割を果たすのは、利用者がブラウザで入力するID・パスワードとMFA承認です。プログラム側は「私はこのアプリです」と名乗るだけ。名乗るだけなら秘密は要りません。
ここで混同しやすいのが client_id ですが、これは公開情報であって秘密ではありません。
-
client_id= お店の名前(看板に出ている。誰が知ってもいい) -
client_secret= 金庫の鍵(絶対に隠す)
仮に client_id が漏れたとして、何ができるか考えてみると、
- 同じ
client_idでデバイスコードフローを開始できる - でも「このURLでコードを入力してログインしてください」と言われる
- 正規の社員のID・パスワードとMFAを突破しない限り、1バイトもデータは取れない
つまり実害がありません。攻撃者が突破すべきものは「本人のアカウント」であり、それはEntra ID側でMFAや条件付きアクセスに守られています。
結果として、マクロブックに書き込むのは client_id とテナントIDだけ。どちらも公開情報なので、ファイルを解析されようが他人のPCにコピーされようが、それだけでは何も起きません。
案1で積み上げようとしていた対策(DPAPIで暗号化、証明書ストアに退避、IP制限、シークレットの定期ローテーション)が、まるごと不要になりました。
余談:あのCLIのログインと同じ原理
ところでこの話、開発者にはかなり馴染みがあるはずです。
CLIツールの初回ログインで、ブラウザが開いて認証すると、ターミナルに戻ったときにはログイン済みになっている——あの体験です。クラウドサービス系のCLIでよく見かけますね。
あれもまったく同じ思想で動いています。
- 利用者の手元で動く = パブリッククライアント
- だからクライアントシークレットを持たない
- 身元の証明はブラウザでログインする人間が担当
- 取得したトークンをローカルにキャッシュして以降は自動
ただし、細部の方式は分岐します。「ブラウザでログインし終わったことを、どうやってCLI側が知るか」の実現手段が2通りあるためです。
| 認可コードフロー + PKCE (ループバック方式) |
デバイスコードフロー | |
|---|---|---|
| 仕組み | CLIが localhost:52341 のようなポートを一時的に開いて待ち受ける。認証後ブラウザがそこへリダイレクトしてくる |
CLIが自分からサーバーへ「もう終わった?」と聞きに行く(ポーリング) |
| ローカルの待ち受け | 必要 | 不要 |
| ブラウザ | CLIが自動で開く | 利用者が自分で開く |
| 主な用途 | デスクトップアプリ、CLI | TV・ゲーム機・SSH越しの環境 |
ブラウザが自動で開き、認証した瞬間にターミナルが反応するタイプは、前者のループバック方式です。裏でポートを開いて待っているからこそ即座に反応できるわけですね。そしてこの手のCLIの多くは、SSH接続などブラウザを開けない環境向けに「コードを表示して手で貼り付ける」フォールバックも備えています。こちらはデバイスコードフローに近いUXです。
では、なぜ我々はループバック方式ではなくデバイスコードフローを選んだのか。環境の制約が違うからです。
CLIツールは、そもそもポートを開くのが自然な存在です。プロセスとして常駐しているし、ネットワーク処理も普通に書ける。ループバック方式のコストが低い。
一方、Excelのマクロでポートを開こうとすると、
- VBAでTCPリッスンを素直に書けない(Win32 APIを直接叩くか外部コンポーネント頼み)
- ファイアウォールが「Excelがネットワーク待ち受けを開始しました」と警告を出しかねない
- ポート競合の考慮も要る
- そもそも設計方針に「サーバープロセスは存在しない」と明記してある
対してデバイスコードフローは、HTTP POSTを数秒おきに投げるだけ。VBAが最も得意な形(WinHttpRequest で外向きリクエストを送るだけ)に収まります。
同じ「ブラウザで人間にログインさせる」思想でも、
CLIはループバックが自然、VBAはポーリングが自然
ちなみにループバック方式には PKCE(ピクシー)という仕組みがセットで使われます。シークレットを持てないパブリッククライアントでも安全に認可コードを交換するための技術で、「毎回その場でランダムな合言葉を作り、認証開始時にそのハッシュを預けておき、トークン交換時に元の値を提示する」という方式です。使い捨てなので事前の埋め込みが要らない。
デバイスコードフローではPKCEを使いません。その場で発行される device_code 自体が使い捨ての秘密として同じ役割を果たすためです。アプローチは違えど、「固定の秘密を埋め込まず、その場限りの秘密で成立させる」という目的は共通ですね。
2回目以降は自動
「毎回ブラウザでログインは面倒では?」という点ですが、⑦で リフレッシュトークン も一緒に受け取れます。
アクセストークンの寿命は1時間程度と短いですが、リフレッシュトークンを保管しておけば、期限切れのたびに裏で自動的に新しいアクセストークンを取り直せます。利用者から見れば「つなぎっぱなし」に見える。
ブラウザでのログインが必要になるのは、初回と、リフレッシュトークンが失効したとき(数十日に一度程度)だけです。
比較して、決めた
最終的に、両案をこう整理しました。
| アプリのみ認証 | 委任認証(デバイスコードフロー) | |
|---|---|---|
| 共有シークレットの漏洩リスク | あり(要多層対策) | なし(そもそも存在しない) |
| 初回起動 | 完全自動 | 利用者がブラウザで一度ログイン |
| 2回目以降 | 完全自動 | 自動(トークンキャッシュ)/数十日に一度再ログイン |
| MFA | 影響なし | 影響なし(むしろ自然に通せる) |
| ローカルサーバー | 不要 | 不要 |
| 管理者の初期設定 | アプリに権限を1回付与するだけ | 利用者ごとにアクセス権付与が必要 |
| 監査ログ | 「アプリがアクセスした」としか残らない | 誰がアクセスしたか実名で残る |
| 退職者の遮断 | ツール側で対応が必要 | アカウント無効化だけで済む |
委任認証(デバイスコードフロー)を第一候補とすることにしました。
決め手は2つです。
- 共有シークレットという構造的な弱点が消える。緩和策を積み上げる必要がない
- 監査ログに実名が残る。顧客データを扱う以上「誰がいつ見たか」が追えるのは大きい。退職者の遮断もアカウント停止だけで完結する
デメリットは「情シスに利用者ごとの権限設定をお願いする手間」と「初回のログイン操作」ですが、10人規模なら十分許容できる範囲だと判断しました。
おまけとして、このツールには元々「Windowsのログイン名から担当者を特定する」仕組みがあるのですが、将来的には委任ログインで得た本人情報をそのまま担当者識別に使うという統合もできそうです(これは追々)。
ハマりどころの整理
同じ道を通る人向けに、要点をまとめておきます。
1. 配布するプログラムは、構造上パブリッククライアント
今回の遠回りの原因は、突き詰めるとこれを最初に意識していなかったことでした。
利用者のPCに配るファイルは、どう頑張っても中身を隠しきれません。その時点で「秘密を持てない側(パブリッククライアント)」に分類が確定している。にもかかわらず、機密クライアント向けの方式(アプリのみ認証)を選ぼうとしたから、「どう隠すか」という答えの出ない問題に突入していました。
設計の初手で「自分が作っているのはどちら側のクライアントか」を判定しておくと、選べる認証方式が自動的に絞られます。
2. 「認証」と「利用者の識別」は別レイヤー
ここは自分でも一度混乱しました。
- 認証(SharePointへのアクセス許可をどう得るか)
- 業務上の利用者識別(「今操作しているのは営業のBさんだ」と特定する)
この2つは別の仕組みです。仮にアプリのみ認証で「全員が同じ資格でSharePointにアクセスする」形にしたとしても、ツール側で担当者を識別して権限を出し分ける機能はそのまま生きます。「認証を一本化したら全員同じ画面になる」わけではありません。
3. ハッシュ化できる秘密/できない秘密
- 照合するだけの秘密(パスワード保管など)→ ハッシュ化できる
- 使うたびに本物を提示する秘密(APIキー、クライアントシークレット)→ ハッシュ化できない。暗号化(=復号できる形)しか選べない
4. クラウド移行でも「データの正本」は1箇所に
余談ですが、移行を検討する中で「クラウドにリストを置きつつ、ローカルのExcelと同期する」という案も浮かびました。が、これはやめました。
同期は必ず競合と不整合を生みます。 結局「どっちが正しいデータか」を判定するロジックが必要になり、複雑さが跳ね上がる。
移行後は「データの正本はクラウドのリストのみ、ローカルには表示用の一時データしか持たない(起動時に取得し、閉じたら消える)」という形に統一しました。ローカルDBは廃止です。
おわりに
「Excelマクロで社内ツールを作る」という、一見レガシーな題材でも、クラウド連携が絡んだ瞬間に OAuth 2.0、MFA、シークレット管理 といった現代的な設計判断が必要になります。
そして今回いちばんの学びは、
「秘密をどう守るか」で悩んだら、「そもそも秘密を持たない構成にできないか」を先に考える
ということでした。
案1で「シークレットをどう隠すか」を延々と考えていたときは、暗号化だのDPAPIだの条件付きアクセスだのと対策が積み上がる一方でした。でも認証方式そのものを変えたら、その悩みが根こそぎ消えた。
対策を重ねる前に、前提を疑う。当たり前のようでいて、なかなかできないやつです。イーロンの推奨する「第一原理」がまさにこれですね。
なお、現時点ではまだ 設計方針を固めた段階 で、実装はこれからです。実際に作ってみたら「デバイスコードフローのポーリングをVBAでやると画面が固まる」みたいな新しい難儀が待っている気もするので、そのときはまた記事にします。
