はじめに
ServiceNowのインシデント情報をREST APIで取得したいものの、環境によってはログイン時にMFA(TOTP)が必須になっていて、単純なBasic認証だけでは弾かれることがあります。
本記事では、Basic認証のパスワード部分に パスワード + TOTP(6桁) を連結する方式で、Pythonからインシデント一覧を取得するサンプルを紹介します。
(※この方式が使えるかどうかは、組織のSSO/MFA設定・認証方式に依存します)
以下のような場合に活用できます。
- ServiceNowのREST API(Table API)をPythonで叩きたい
- MFA(TOTP)が必須の環境で、スクリプトからの呼び出しに困っている
- 「パスワード+ワンタイムコード」を同時入力するタイプの認証を自動化したい
前提・注意事項(重要)
- TOTPシークレット(共有鍵)は漏洩厳禁です。流出するとMFAを生成できてしまいます。
- 会社/組織のルールによっては、MFAの自動化そのものが禁止の場合があります。公開・運用前に必ず確認してください。
- 本記事は「技術的にこうすれば動くケースがある」という紹介であり、セキュリティ上の責任は各自でご対処願います。
やりたいこと
- PythonでTOTP(6桁)を生成
-
パスワード + TOTP(6桁)を作る - ServiceNowのTable API(incident)をBasic認証で呼び出す
- インシデント番号・件名・作成日時を表示する
使うもの
- Python 3.x
- ライブラリ
requestspyotp
インストール:
pip install requests pyotp
環境変数の用意(取扱注意)
記事では伏せていますが、実運用では以下のように環境変数で渡します。サンプルのコードで直接書いて試すことはできますが、業務で使う場合は別ファイルで機密情報として扱ってください。
export SN_INSTANCE_URL="https://<your-instance>.service-now.com"
export SN_USERNAME="<your-username>"
export SN_PASSWORD="<your-password>"
export SN_TOTP_SECRET="<your-totp-secret-without-spaces>"
TOTPシークレットについて
Authenticatorアプリ等で表示されるシークレットにスペースが含まれている場合は、pyotpに渡す前にスペースを除去した文字列を使います。
ServiceNowの場合、MFA初回登録の際にQRコードの下段に
実装例(インシデント一覧を取得)
ポイントはここです:
-
pyotp.TOTP(TOTP_SECRET).now()で 現在時刻のワンタイムコードを作る -
PASSWORDと結合してcombined_passwordを作る - Basic認証のパスワードとして
(USERNAME, combined_password)を渡す
import os
import requests
import pyotp
# ====== 設定(Qiita掲載向け:環境変数から取得) ======
INSTANCE_URL = os.getenv("SN_INSTANCE_URL", "https://<your-instance>.service-now.com")
USERNAME = os.getenv("SN_USERNAME", "<your-username>")
PASSWORD = os.getenv("SN_PASSWORD", "<your-password>")
TOTP_SECRET = os.getenv("SN_TOTP_SECRET", "<your-totp-secret-without-spaces>")
def build_combined_password() -> str:
"""PASSWORD + TOTP(6桁) を結合したパスワードを生成"""
totp = pyotp.TOTP(TOTP_SECRET)
mfa_code = totp.now()
# MFAコードはログに出すと危険なので、基本は出さない
# print(f"[DEBUG] MFA code: {mfa_code}")
return f"{PASSWORD}{mfa_code}"
def fetch_incidents(limit: int = 10) -> None:
"""インシデントを一覧取得して表示"""
combined_password = build_combined_password()
api_url = f"{INSTANCE_URL}/api/now/table/incident"
params = {
"sysparm_limit": limit,
"sysparm_fields": "number,short_description,sys_created_on",
}
# Basic Auth(ユーザー名 / パスワード+MFAコード)
auth = (USERNAME, combined_password)
headers = {"Accept": "application/json"}
response = requests.get(
api_url,
auth=auth,
headers=headers,
params=params,
timeout=30,
)
if response.status_code == 200:
data = response.json()
records = data.get("result", [])
print(f"[+] 取得件数: {len(records)}")
print("-" * 60)
for r in records:
print(f"{r.get('number')} — {r.get('short_description')} ({r.get('sys_created_on')})")
else:
print("[!] Error:", response.status_code)
print(response.text)
if __name__ == "__main__":
fetch_incidents(limit=10)
実行結果例(イメージ)
[+] 取得件数: 10
------------------------------------------------------------
INC0012345 — VPNに接続できない (2025-01-01 10:00:00)
INC0012346 — メール送信が遅い (2025-01-01 10:05:00)
...
仕組みの補足(認証フローのイメージ)
「ログイン画面でパスワードとワンタイムコードを同じ入力欄に入れる」運用のAPI版、という理解です。
ハマりどころ
1) 時刻ズレでTOTPが合わない
TOTPは時刻依存なので、実行環境の時計がズレると失敗します。
まずはNTP等で時刻同期を確認します。
2) TOTPシークレットの形式
シークレットにスペースが入っている/改行が混ざる等で失敗することがあります。
スペース除去、余計な文字が入っていないかを確認します。
3) 組織設定により、この方式がそもそも通らない
環境によっては、Basic認証ではなくOAuth/SSO専用、あるいはMFAが別チャネル扱いで「パスワード連結」では通らない場合もあります。
セキュリティ上の注意(再掲)
- TOTP_SECRETをGitに入れない
- MFAコードをログに出さない
- 可能なら Secret Manager / Vault / CIのシークレット機能を使う
- 運用ルール・監査要件に従う
まとめ
-
pyotpでTOTP(ワンタイムコード)を生成し、パスワード + TOTPとしてBasic認証に渡すことで、MFAQ環境でもAPI取得ができるケースがあります - 記事化する際は、秘密情報を直書きしない・ログに出さないが重要です
難しい外部連携をするより、自分のコードでレコード情報を一括でエクスポートできるため、業務効率の向上に貢献できると思います。
以上です