Microsoft Graph API + PythonでAzure ADのサインインログを取得してみた

2022.02.19

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

どうも、ベルリンオフィスの小西です。

最近Mirosoft Graph APIを触ることがありました。

日本語の情報があまりなかったので、これをいい機会に認証方法やデータの取得方法をまとめてみたいと思います。

Microsoft Graphとは

Microsoftの中の人がQiitaで記事を書いてくれているので引用します。

Microsoft のサービスである Office 365 や Azure AD など、様々なサービスのデータをグラフ形式で扱えるエンドポイントです。グラフ形式でデータを扱えるので、「自分の予定」や「自分の上司」などデータを関連から取得できます。

単一のREST APIエンドポイントでMicrosoftの各サービスのデータを取得・処理でき、認証も一括で管理できるため非常に使いやすいと感じました。

なお、これに伴い Microsoft Azure AD Graph や Outlook REST API ・ Discovery Service APIsなどのOffice 365 API は Microsoft Graph への移行を推奨されています。

例えば下記のようなことが可能です(あくまで一例)。

  • 自分のプロファイルの取得: https://graph.microsoft.com/v1.0/me
  • 自分のメールの取得: https://graph.microsoft.com/v1.0/me/messages
  • 自分の所属組織のユーザーの取得: https://graph.microsoft.com/v1.0/users

Microsoft Graph Explorer

GUIで簡単に Graph API リクエストとレスポンスを確認できる Graph エクスプローラー というサービスもあります。

https://developer.microsoft.com/ja-jp/graph/graph-explorer

microsoft graph

アカウントに許可されている権限によって取得できないデータがありますが、まずは下記のエンドポイントによる自分の情報の取得はどなたでも試せますので、どんな感じでGraphがデータを返すか見ていただければと思います。

https://graph.microsoft.com/v1.0/me

細かい使い方などはこちら: https://docs.microsoft.com/ja-jp/graph/graph-explorer/graph-explorer-overview

事前準備: Microsoftのテナントトークン取得

1. Developer Accountの作成

MicrosoftのDeveloper Accountに登録します(無料)。下記開発者用ページの [今すぐ参加] から登録します。

https://developer.microsoft.com/ja-jp/microsoft-365/dev-program

登録が完了してポータルから確認し、

Microsoft 365 E5 Developer

となっていればOKです。

Overviewでもライセンスが [Azure AD Premium P2] になっています。これは後で紹介するAzure ADのサインインログの取得のために必要なライセンスになります。

microsoft graph

2. Appの作成

https://portal.azure.com/ にログインし、[App Registration]に移動します

その後[Register an application]からアプリを登録します。

microsoft graph

Appが作成されたら、後で使う[Application (client) ID]と[Directory (tenant) ID]をメモしておきます。
また[Add a certificate or secret]をクリックして進みます。

microsoft graph

secretが発行されたら value をコピーします(ページを移動すると見えなくなるので忘れずに)。

microsoft graph

また次に、AppにAzure ADの読み取り権限を与えます。

microsoft graph

デフォルトでUser.Readは付与されていますが、下記の権限も付与します。

  • Directory.Read.All
  • AuditLog.Read.All

microsoft graph

※上記の同じ画面でAuditLog.Read.Allも付与する

また、上記で取得した権限に同意を付与します(これをしないと権限が付与されない)。

microsoft graph

下記の通り、Directory.Read.All と AuditLog.Read.All が Grantedになれば準備は完了です。

microsoft graph

実践: PythonからAPIを叩く

1. 準備: 認証用パッケージの追加

Pythonからアクセスする際の認証トークンを取得する必要があります。

Python用には MSAL (Microsoft Authentication Library) が公開されていますのでそれを利用します。

https://github.com/AzureAD/microsoft-authentication-library-for-python

パッケージをインストールします。

% pip install msal

2. ユーザーアカウント一覧の取得

コードは下記になります。 tenant_id, client_id, client_secret に先ほどAzureポータルのダッシュボードからコピーした値を貼り付けます。

処理の中身としては、

  • msgraph_auth() でアクセストークンを取得
  • msgraph_request() で ADに紐づくユーザー一覧を取得するようGraphにリクエスト
  • 返り値の中から PrincipalName をリスト表示

という感じです。

import json 
import requests
import msal
import traceback

#Graph API configuration
graph_url = 'https://graph.microsoft.com'
tenant_id = "XXXXX"
client_id = "XXXXX"
client_secret = "XXXXX"

def msgraph_auth():
    authority = 'https://login.microsoftonline.com/' + tenant_id
    scope = ['https://graph.microsoft.com/.default']

    try:
        app = msal.ConfidentialClientApplication(client_id, authority = authority, client_credential = client_secret)
        access_token = app.acquire_token_for_client(scopes = scope)
        if access_token['access_token']:
            print('New access token retrieved....')
            request_headers = {'Authorization': 'Bearer ' + access_token['access_token']}
            return request_headers
        else:
            print('ERROR: No "access_token" in the result.')
    except:
        print('ERROR: Could not acquired authorization token. Check your tenant_id, client_id and client_secret.')
        traceback.print_exc()
        exit(1)

def msgraph_request(resource,request_headers):
    results = requests.get(resource, headers = request_headers).json()
    return results

def show_users():

    # ユーザー一覧を取得するエンドポイントのパス
    q = "users"

    # Graphへのクエリ実行
    request_headers = msgraph_auth()
    results = msgraph_request(graph_url + '/v1.0/' + q, request_headers)

    if results:
        try:
            for r in results["value"]:
                print("userPrincipalName:", r["userPrincipalName"])
        except:
            print('ERROR: No "value" or "userPrincipalName" in the result.')
            traceback.print_exc()
            exit(1)

show_users()

実行してみます。

% python get-azure-ad-users.py
New access token retrieved....
userPrincipalName: AdeleV@wzhtb.onmicrosoft.com
userPrincipalName: AlexW@wzhtb.onmicrosoft.com
userPrincipalName: DiegoS@wzhtb.onmicrosoft.com
userPrincipalName: GradyA@wzhtb.onmicrosoft.com
userPrincipalName: HenriettaM@wzhtb.onmicrosoft.com
userPrincipalName: IsaiahL@wzhtb.onmicrosoft.com
userPrincipalName: JohannaL@wzhtb.onmicrosoft.com
userPrincipalName: JoniS@wzhtb.onmicrosoft.com
userPrincipalName: LeeG@wzhtb.onmicrosoft.com
userPrincipalName: LidiaH@wzhtb.onmicrosoft.com
userPrincipalName: LynneR@wzhtb.onmicrosoft.com
userPrincipalName: MeganB@wzhtb.onmicrosoft.com
userPrincipalName: MiriamG@wzhtb.onmicrosoft.com
userPrincipalName: NestorW@wzhtb.onmicrosoft.com
userPrincipalName: PattiF@wzhtb.onmicrosoft.com
userPrincipalName: PradeepG@wzhtb.onmicrosoft.com
userPrincipalName: rk-cmeu@wzhtb.onmicrosoft.com

成功!

サンプルで登録されているユーザーと自分の userPrincipalName (メールアドレス)が取得できました。サンプルデータが入っているのはいいですね。

3. サインインログの取得

例えばダッシュボードの下記からAD所属ユーザーのアクティビティが確認できるのですが、これをAPIでも取得できます。

microsoft graph

注意:

サインインログや監査ログは Activity reports API の一部となり、プレミアムライセンス(P1/P2)が必要です。本記事で紹介したDeveloperアカウントであればプレミアムライセンス相当の権限があるので問題ありませんが、ご自身のビジネスアカウントでお試しいただいている際は、Neither tenant is B2C or tenant doesn't have premium license というエラーメッセージが返ってきてしまいますのでご注意ください。

コードは先ほどとほぼ一緒ですが、先ほどのPythonコードの36行目以降(show_users())を下記コードと置き換えてください。

エンドポイントパスを変更しているのと、ログの期間をフィルターしています(2022/02/12から02/18終日までのデータのみ取得)が、このフィルターについては次で詳しく説明します。

def show_signin_logs():

    # サインインログを取得するエンドポイントのパス
    q = "auditLogs/signIns"

    # データのフィルタリング
    f = "(createdDateTime ge 2022-02-12T00:00:00Z and createdDateTime le 2022-02-18T23:59:59Z)"

    # Graphへのクエリ実行
    request_headers = msgraph_auth()
    results = msgraph_request(graph_url + '/v1.0/' + q + '?$filter=' + f, request_headers)

    if results:
        try:
            for r in results["value"]:
                print(r["createdDateTime"], "|", r["userPrincipalName"])
        except:
            print('ERROR: No "value", "createdDateTime" or "userPrincipalName" in the result.')
            traceback.print_exc()
            exit(1)

show_signin_logs()

上記実行してみます。

% python get-azure-ad-singin-logs.py
New access token retrieved....
2022-02-18T11:32:22Z | rk-cmeu@wzhtb.onmicrosoft.com
2022-02-18T11:32:21Z | rk-cmeu@wzhtb.onmicrosoft.com
2022-02-18T11:32:21Z | rk-cmeu@wzhtb.onmicrosoft.com
2022-02-18T11:31:52Z | rk-cmeu@wzhtb.onmicrosoft.com
2022-02-18T11:31:52Z | rk-cmeu@wzhtb.onmicrosoft.com
2022-02-18T11:31:52Z | rk-cmeu@wzhtb.onmicrosoft.com
2022-02-18T11:31:47Z | rk-cmeu@wzhtb.onmicrosoft.com
2022-02-18T11:02:17Z | rk-cmeu@wzhtb.onmicrosoft.com
2022-02-18T11:02:17Z | rk-cmeu@wzhtb.onmicrosoft.com
2022-02-18T11:02:17Z | rk-cmeu@wzhtb.onmicrosoft.com
2022-02-18T11:01:59Z | rk-cmeu@wzhtb.onmicrosoft.com
2022-02-18T10:59:23Z | rk-cmeu@wzhtb.onmicrosoft.com

無事、特定期間のサインインログからログイン時間とユーザーメアドが取得できました。

なお上記ではデータをシンプルに改変して出力していますが、返り値全体としては下記のようになります。

{
    "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#auditLogs/signIns",
    "@odata.nextLink": "https://graph.microsoft.com/v1.0/auditLogs/signIns?$top=1&$skiptoken=9177f2e3532fcd4c4d225f68f7b9bdf7_1",
    "value": [
        {
            "id": "66ea54eb-6301-4ee5-be62-ff5a759b0100",
            "createdDateTime": "2020-03-13T19:15:41.6195833Z",
            "userDisplayName": "Test Contoso",
            "userPrincipalName": "testaccount1@contoso.com",
            "userId": "26be570a-ae82-4189-b4e2-a37c6808512d",
            "appId": "de8bc8b5-d9f9-48b1-a8ad-b748da725064",
            "appDisplayName": "Graph explorer",
            "ipAddress": "131.107.159.37",
            "clientAppUsed": "Browser",
            "correlationId": "d79f5bee-5860-4832-928f-3133e22ae912",
            "conditionalAccessStatus": "notApplied",
            "isInteractive": true,
            "riskDetail": "none",
            "riskLevelAggregated": "none",
            "riskLevelDuringSignIn": "none",
            "riskState": "none",
            "riskEventTypes": [],
            "resourceDisplayName": "Microsoft Graph",
            "resourceId": "00000003-0000-0000-c000-000000000000",
            "status": {
                "errorCode": 0,
                "failureReason": null,
                "additionalDetails": null
            },
            "deviceDetail": {
                "deviceId": "",
                "displayName": null,
                "operatingSystem": "Windows 10",
                "browser": "Edge 80.0.361",
                "isCompliant": null,
                "isManaged": null,
                "trustType": null
            },
            "location": {
                "city": "Redmond",
                "state": "Washington",
                "countryOrRegion": "US",
                "geoCoordinates": {
                    "altitude": null,
                    "latitude": 47.68050003051758,
                    "longitude": -122.12094116210938
                }
            },
            "appliedConditionalAccessPolicies": [
                {
                    "id": "de7e60eb-ed89-4d73-8205-2227def6b7c9",
                    "displayName": "SharePoint limited access for guest workers",
                    "enforcedGrantControls": [],
                    "enforcedSessionControls": [],
                    "result": "notEnabled"
                },
                {
                    "id": "6701123a-b4c6-48af-8565-565c8bf7cabc",
                    "displayName": "Medium signin risk block",
                    "enforcedGrantControls": [],
                    "enforcedSessionControls": [],
                    "result": "notEnabled"
                },
              ]
        }
    ]
}

クエリのカスタマイズ

Microsoft Graph ではオプションとして、リクエストURLにクエリパラメーターを付与することでレスポンスをカスタマイズすることができます。

データのフィルタリング、並び替え、カウント、スキップなどかなり色々なことがAPIリクエスト時点で指定できるのは嬉しい点です。

Name 説明
$count 一致するリソースの総数を取得します。 /me/messages?$top=2&$count=true
$expand 関連リソースを取得します。 /groups?$expand=members
$filter 結果 (行) をフィルターします。 /users?$filter=startswith(givenName,'J')
$format 指定したメディア形式で結果を返します。 /users?$format=json
$orderby 結果を並べます。 /users?$orderby=displayName desc
$search 検索条件に基づいて結果を返します。 /me/messages?$search=pizza
$select プロパティ (列) をフィルターします。 /users?$select=givenName,surname
$skip 結果セットにインデックスを作成します。また一部の API でページングを実装するために使用されており、$top と組み合わせて手動で結果をページングすることもできます。 /me/messages?$skip=11
$top 結果のページ サイズを設定します。 /users?$top=2

詳しい仕様は https://docs.microsoft.com/ja-jp/graph/query-parameters からご確認ください。

先ほどの例ではフィルターを下記のようにしていました。

?$filter=(createdDateTime ge 2022-02-12T00:00:00Z and createdDateTime le 2022-02-18T23:59:59Z)

上記は「UCT2022-02-12の00:00:00以降で(2022-02-12の00:00:00ピッタリを含む)、かつ2022-02-18の23:59:59より以前(2022-02-18の23:59:59ピッタリを含む)のデータ」にフィルタリングをしています。

フィルタリングしたいデータによって利用できる属性が異なりますので、下記を確認して使う必要があります。

https://docs.microsoft.com/ja-jp/graph/aad-advanced-queries

例えば createdDateTime に対して ge (以上)や le (以下)は使えますが、 文字列ではないので startsWith (文字が**から始まる)や contains (文字列を含む)は使えません。

最後に

Microsoft OfficeのAPIをいじるにはSubscriptionが複雑な印象でしたが、諸々の権限を付与可能でサンプルデータも用意されているDeveloperアカウントがあるのは非常に助かりました。Graph自体はとても使いやすく、今後も知見がたまったら共有していきたいと思います。

参考記事まとめ