💻

MSAL for Python を使用して Azure AD の最終サインイン日時を取得する

2022/04/17に公開

この記事について

この記事では、Microsoft Authentication Library for Python (MSAL for Python) を使用して、Microsoft Graph API 経由で Azure AD 上に存在するユーザーのサインインログ情報を参照し、各ユーザーの最終サインイン日時 (UTC) を取得する手順について記載しています。

環境

今回は、以下の環境で動作確認をしています。
著者は Docker コンテナー上にて実行していますが、Docker については必須要件ではありません。また、Windows など他の OS 環境でも同様に実行可能な内容です。

  • OS: macOS Monterey バージョン 12.3.1 (Intel)
  • Docker: Docker for Mac (Docker Engine v20.10.13)
  • Python: 3.10.4
    • msal 1.17.0
    • requests 2.27.1

サンプルコード

本記事に記載の Python コードをはじめ、MSAL for Python のサンプルコードなどは以下にて公開しています。

https://github.com/ymasaoka/Sample-MSAL-Python

なお、本記事、および、上記リポジトリに載せているサンプルコードでは、取得したデータを print で表示するようにしていますので、実運用などにおいては json や csv ファイルにエクスポートするなど、ユーザー自身で追加の処理を行うようにしてください。

コードの実行に必要なサービスプリンシパル

本記事および上記の GitHub リポジトリに記載のコードを実行するためには、Azure AD にて サービスプリンシパル を事前に準備する必要があります。

https://docs.microsoft.com/ja-jp/azure/active-directory/develop/app-objects-and-service-principals

最終サインイン日時を取得する用のサービスプリンシパルを作成し、以下のアクセス許可を付与したものを用意してください。

  • Microsoft Graph/アプリケーションの許可/AuditLog.Read.All
  • Microsoft Graph/アプリケーションの許可/Directory.Read.All

最終サインイン日時 (UTC) を取得する - Graph API v1.0 を利用

Azure AD 上に保存されるサインインログについては、仕様により 30 日未満の分しか保存できない仕様になっています。そのため、Microsoft Graph API v1.0 を使用する場合は、過去 30 日分の中での最終サインイン日時を取得することが可能です。
最終サインイン日時が 30 日以上経過しているユーザー、および、一度も Azure AD にサインインしていないユーザーについては値を取得することができないため、0 が返るようにしています。

実際にコード内で行っている事としては

  1. Azure AD 上に登録されている全ユーザー情報を取得
  2. 取得したユーザー情報を使用して、1 ユーザーずつ Azure AD のサインインログをクエリ
  3. 最終サインイン日時が取得できればその値を返し、最終サインイン日時を取得できなければ 0 を返す

という感じです。
なお、今回はサービスプリンシパルはクライアントシークレットを用いた形で利用する内容にしています。クライアント ID やクライアントシークレット、テナント ID については OS の環境変数に入れて実行しているものになります。必要に応じて、書き換えなどの対応を行なってください。(ただし、コードへの直書きは非常に危ないのでお勧めしません)

get_auditlogs_lastsignins.py
import os
import msal
import requests

authority = f"https://login.microsoftonline.com/{os.environ['TENANT_ID']}"
scopes = ['https://graph.microsoft.com/.default']
endpoint_users = 'https://graph.microsoft.com/v1.0/users'
endpoint_auditlogs = 'https://graph.microsoft.com/v1.0/auditLogs/signIns'

def connect_aad():
    cred = msal.ConfidentialClientApplication(
        client_id=os.environ['CLIENT_ID'],
        client_credential=os.environ['CLIENT_SECRET'],
        authority=authority)

    return cred

def get_access_token(cred):
    res = None
    res = cred.acquire_token_silent(scopes,account=None)

    if not res:
        print("キャッシュ上に有効なアクセストークンがありません。Azure AD から最新のアクセストークンを取得します。")
        res = cred.acquire_token_for_client(scopes=scopes)

    return res

def get_users_all(token):
    p = '?$select=id,userPrincipalName'
    res = requests.get(
        f"{endpoint_users}{p}",
        headers={'Authorization': 'Bearer ' + token['access_token']})

    if res.ok:
        data = res.json()
        print(f"{len(data['value'])} 件のユーザー情報取得に成功しました。")
        return data
    else:
        print(res.json())

def get_auditlog_lastsingin(token, users):
    for user in users['value']:
        q = f"?$filter=userId eq '{user['id']}'&$orderby=createdDateTime desc&$top=1"
        res = requests.get(
            f"{endpoint_auditlogs}{q}",
            headers={'Authorization': 'Bearer ' + token['access_token']})
        data = res.json()

        if res.ok & len(data['value']) > 0:
            data = res.json()
            print(f"UPN: {data['value'][0]['userPrincipalName']}, 最終ログイン日時(UTC): {data['value'][0]['createdDateTime']}")
        elif res.ok & len(data['value']) == 0:
            print(f"UPN: {user['userPrincipalName']}, 最終ログイン日時(UTC): 0")
        else:
            print(data)

def main():
    cred = connect_aad()
    token = get_access_token(cred)

    if "access_token" in token:
        users = get_users_all(token)
        get_auditlog_lastsingin(token, users)
    else:
        print(token.get("error"))
        print(token.get("error_description"))
        print(token.get("correlation_id"))

if __name__ == '__main__':
    main()

最終サインイン日時 (UTC) を取得する - Graph API beta を利用

Graph API v1.0 を使用した場合では、先述の通り、以下のデメリットがありました。

  • 過去 30 日以上サインインしていないユーザーの日時は取れない (Azure AD 上のサインインログは 30 日以上経過した分は削除されてしまう)
  • ユーザーごとに Azure AD 上のサインインログをクエリしなければならず、処理が非効率 (ユーザー数が多ければ多いほど処理がループする)

そのため、Graph API beta では、30 日以上サインインしていないユーザーの分も含め、ユーザーの最終サインイン日時 (UTC) が取得できるように改善が行われています。
Graph API beta では、ユーザー情報取得の際に v1.0 では取得できない signInActivity データを取得することが可能となっており、こちらの中にある最終サインイン日時を取得することになります。
signInActivity については、v1.0 の時のような 30 日制限などはありませんが、通常では見えない項目のため、明示的に値を取得する必要があります。

なお、こちらも v1.0 の時と同様、サービスプリンシパルはクライアントシークレットを用いた形で利用する内容にしています。クライアント ID やクライアントシークレット、テナント ID については OS の環境変数に入れて実行しているものになります。必要に応じて、書き換えなどの対応を行なってください。(ただし、コードへの直書きは非常に危ないのでお勧めしません)

get_auditlogs_lastsignins_beta.py
import os
import msal
import requests

authority = f"https://login.microsoftonline.com/{os.environ['TENANT_ID']}"
scopes = ['https://graph.microsoft.com/.default']
endpoint_users = 'https://graph.microsoft.com/beta/users'

def connect_aad():
    cred = msal.ConfidentialClientApplication(
        client_id=os.environ['CLIENT_ID'],
        client_credential=os.environ['CLIENT_SECRET'],
        authority=authority)

    return cred

def get_access_token(cred):
    res = None
    res = cred.acquire_token_silent(scopes,account=None)

    if not res:
        print("キャッシュ上に有効なアクセストークンがありません。Azure AD から最新のアクセストークンを取得します。")
        res = cred.acquire_token_for_client(scopes=scopes)

    return res

def get_users_all_and_lastsignins(token):
    q = '?$select=id,userPrincipalName,userType,signInActivity'
    res = requests.get(
        f"{endpoint_users}{q}",
        headers={'Authorization': 'Bearer ' + token['access_token']})

    if res.ok:
        data = res.json()
        print(f"{len(data['value'])} 件のユーザー情報と最終サインイン日時の取得に成功しました。")
        print(data)

        for user in data['value']:
            print(f"UPN: {user['userPrincipalName']}, ユーザー種別: {user['userType']}, 最終ログイン日時(UTC): {user['signInActivity']['lastSignInDateTime']}")
            # user = {
            #     'userPrincipalName': '***',
            #     'userType': 'Member', # Member or Guest
            #     'id': '***',
            #     'signInActivity': {
            #         'lastSignInDateTime': '2021-06-27T16:03:52Z',
            #         'lastSignInRequestId': '18d3b341-727f-418b-995f-3be4bf11bd00',
            #         'lastNonInteractiveSignInDateTime': '0001-01-01T00:00:00Z',
            #         'lastNonInteractiveSignInRequestId': ''
            #     }
            # }
    else:
        print(res.json())

def main():
    cred = connect_aad()
    token = get_access_token(cred)

    if "access_token" in token:
        get_users_all_and_lastsignins(token)
    else:
        print(token.get("error"))
        print(token.get("error_description"))
        print(token.get("correlation_id"))

if __name__ == '__main__':
    main()

参考情報

Japan Azure Identity Support Blog

GitHub

Microsoft 公式

ymasaoka

Discussion