🌥️

Microsoft.Graph を用いた OneDrive 上のフォルダにあるファイルを検索するアプリケーションの開発(C#)

2023/12/10に公開

2023/12/10 現在の内容です。
C# を用いた Windows アプリの開発に携わっています。「クラウド上に保存したファイルの中身を確認する」という機能について技術調査しましたので議事録を兼ねて結果をまとめます。☁

Microsoft.Graph を用いた OneDrive 上のフォルダにあるファイルを確認するアプリケーションの開発(C#)

今回は開発者がサンプルアカウントを用いて OneDrive 上のフォルダにあるファイルを確認するアプリケーション開発の一連の流れについて記載します。大まかな流れは下記になります。

  1. 開発者用アカウントの作成
  2. アプリケーションの登録
  3. アプリケーションの設定
  4. サインイン
  5. ファイルアクセス

1. 開発者用アカウントの作成

Microsoft 365 developer Program がオススメです。

このProgramにより、アプリケーション(AAD/M365も含みます)を
設計、開発、およびテストするためならば、申し込むことで90日間AzureADの機能等が無料で試せます。

https://qiita.com/kaiinaba/items/b619eea6e9198772c58b

アカウント作成方法

下記2サイトを参考にしました。

  • Microsoft

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

  • 分かりやすいもの

https://kuroivlog.com/microsoft365_e5_enviroment

2. アプリケーションの登録

Microsoft Entra ID(旧 Azure Active Directory) にアプリケーションを登録して、認証で使用する アプリケーション(クライアント) ID 及び ディレクトリ(テナント) ID を取得します。

  • アプリケーション(クライアント) ID
    ID プラットフォーム内でアプリケーションとその構成を一意に識別する GUID

  • ディレクトリ(テナント) ID
    リソースへのアクセスを認証および承認のための一意に識別する GUID

登録方法

下記サイトを参考にしました。
https://www.ipentec.com/document/microsoft-azure-register-application-in-azure-active-directory

3. アプリケーションの設定

Microsoft Entra ID で登録したアプリケーションの設定をいくつか行う必要があります。
全て「Penta」さんという方の記事を参考にしています。

  1. パブリッククライアントフローを有効にする
  2. API のアクセス許可に対して管理者の同意を付与する
  3. API のアクセスを許可する

パブリッククライアントフローを有効にする

https://www.ipentec.com/document/microsoft-azure-application-allow-public-client-flow

API のアクセス許可に対して管理者の同意を付与する

https://www.ipentec.com/document/microsoft-azure-application-allow-access-permission

API のアクセスを許可する

https://www.ipentec.com/document/microsoft-azure-add-api-permissions-to-azure-active-directory-application

4. サインイン

フォルダへのアクセスを行う前に Microsoft.Identity.Client.dll を用いてサインインするための認証を行います。Microsoft Entra ID(旧 Azure Active Directory) にアプリケーションを登録した際の アプリケーション(クライアント) ID 及び ディレクトリ(テナント) ID を指定します。
イメージとしては下記になります。

  • フレームワーク
    .NET Framework4.5
  • 使用ライブラリ
    Microsoft.Identity.Client.dll(ver 4.54.1.0)

簡単なサンプルプログラムは Microsoft が提供しており、複数の認証方法を選択することができます。
https://learn.microsoft.com/en-us/graph/sdks/choose-authentication-providers?tabs=csharp

認証方法 説明
Authorization code provider ネイティブアプリとウェブアプリは、ユーザー名で安全にトークンを取得できるようになります。
Client credentials provider サービスアプリケーションはユーザーとの対話なしで実行できます。アクセスはアプリケーションの ID に基づいて行われます。(クライアント証明 or クライアントシークレット)
On-behalf-of provider アプリケーションがサービス/Web API を呼び出し、Microsoft Graph API を呼び出す場合に使用します。
Device code provider 別のデバイスを経由してデバイスにサインインすることを可能にします。
Integrated Windows provider Windows コンピュータがドメインに参加するときに、アクセストークンを自動で取得する方法を提供します。
Interactive provider モバイルアプリケーション(Xamarin と UWP)とデスクトップアプリケーションで、ユーザーの名前で Microsoft Graph API を呼び出すために使用します。
Username/password provider アプリケーションがユーザー名とパスワードを使用してユーザーにサインインします。このフローは、他の OAuth フローを使用できない場合にのみ使用します。

対話形式認証

UI を作成することなく、下記のようなサインイン画面を表示し、対話形式でアクセストークンを取得できます。

下記を参考にしました。
https://learn.microsoft.com/ja-jp/entra/identity-platform/scenario-desktop-acquire-token-interactive?tabs=dotnet
https://www.ipentec.com/document/csharp-onedrive-upload-file#google_vignette

実装例を下記に示します。動作するために作ったため、体裁はこだわっていませんのでメモとして、、、

// クライアントアプリの構築
	string[] scopes = new string[] { "user.read" };
	PublicClientApplicationBuilder app = PublicClientApplicationBuilder.Create(_clientId);
	app = app.WithRedirectUri("https://login.microsoftonline.com/common/oauth2/nativeclient");
	app = app.WithAuthority(AzureCloudInstance.AzurePublic, _tenantId);
	_publicClientApplication = app.Build();

	AuthenticationResult result = null;

	// クライアント取得
	var accounts = await _publicClientApplication.GetAccountsAsync();
	IAccount account = accounts.FirstOrDefault();

	// 認証済みのアカウントがある場合は AcquireTokenSilent によるトークンの取得を行う
	// トークンが失効していれば新しいトークンを取得し、失効してなければキャッシュされたトークンを返す
	try
	{
	    // AcquireTokenSilent によるトークンの取得を行う
	    result = await _publicClientApplication.AcquireTokenSilent(scopes, account)
		    .ExecuteAsync();
	}
	// 認証済みのアカウントがない場合
	catch (MsalUiRequiredException)
	{
	    try
	    {
	        // AcquireTokenInteractive による対話型認証を行う
	        result = await _publicClientApplication.AcquireTokenInteractive(scopes)
	.ExecuteAsync();

	    }
	    // ID プロバイダー (Azure AD) がエラーを返した場合
	    catch (MsalServiceException ex)
	    {
	    }
	    catch (Exception ex)
	    {
	    }
	}
	catch (Exception ex)
	{
	}
	finally
	{
	    if (result.AccessToken != null)
	    {
	        // サインイン成功
	    }
	    else
	    {
	        // サインイン失敗
	    }
	}

ユーザー名パスワード認証

基本的に推奨されないフローになります。
下記を参照しました。
https://qiita.com/songoku/items/7d35f55e7d574d8ca112

実装例を下記に示します。動作するために作ったため、体裁はこだわっていませんのでメモとして、、、

	var userName = "";
	var password = "";

	string authority = $"https://login.microsoftonline.com/{_tenantId}/v2.0";
	IPublicClientApplication app;

	  // クライアントアプリの構築
	app = PublicClientApplicationBuilder.Create(_clientId)
	      .WithAuthority(authority)
	      .Build();

	  // クライアントアカウント取得
	var accounts = await app.GetAccountsAsync();

	AuthenticationResult result = null;
	  // 要素が含まれている場合
	if (accounts.Any())
	{
	    result = await app.AcquireTokenSilent(_scopes, accounts.FirstOrDefault())
		          .ExecuteAsync();
	}
	  // ユーザー名・パスワードを用いて認証
	else
	{
	    try
	    {
	        result = await app.AcquireTokenByUsernamePassword(_scopes, userName, password)
			   .ExecuteAsync();

	        if (result.AccessToken != null)
	        {
		//サインイン成功
	        }
	    }
	    catch (MsalException ex)
	    {
	    }
	    catch (Exception ex)
	    {
	    }

	}

セキュリティプロトコルによるエラー

対話形式認証を行う際に、 MsalServiceException (認証サービスとの対話中に問題が発生した場合の例外)が発生しました。

原因

Microsoft Entra ID で使用するべきセキュリティプロトコル TLS1.2 が指定されていなかったからです。
(.NET Framework4.5 の規定値:SSL 3.0 および TLS 1.0)
https://learn.microsoft.com/en-us/troubleshoot/azure/active-directory/enable-support-tls-environment?tabs=azure-monitor

対処法

System.Net.ServicePointManager.SecurityProtocol プロパティで明示的に TLS1.2 を指定することで解消されました。

  • 実装例
System.Net.ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12;

参考

OS によって TLS を設定できないバージョンがありますので確認が必要です。

  • .NET Framework 3.5 (3.5.1)
    既定では TLS 1.1 および 1.2 は未対応
    プロパティの既定値は SSL 3.0 および TLS 1.0

  • .NET Framework 4.5.2
    TLS 1.1 / TLS 1.2 に対応済み
    プロパティの既定値は SSL 3.0 および TLS 1.0

  • .NET Framework 4.6.x
    TLS 1.1 / TLS 1.2 に対応済み
    プロパティの既定値は TLS 1.0、1.1 および 1.2

  • .NET Framework 4.7.x
    TLS 1.1 / TLS 1.2 に対応済み
    プロパティの既定値は SystemDefault となり、OS の TLS の設定状態に依存する

  • .NET Framework 4.8.x
    TLS 1.1 / TLS 1.2 に対応済み
    プロパティの既定値は SystemDefault となり、OS の TLS の設定状態に依存する

5. ファイルアクセス

サインインが完了したら、Microsoft Graph dll を用いてファイルにアクセスします。

  • フレームワーク
    .NET Framework4.5
  • 使用ライブラリ
    Microsoft.Graph.dll(ver 1.25.1.0)

下記を参照しました。
https://qiita.com/kenakamu/items/2a56d20f5eb74f0665d1

Microsoft Graph とは

Microsoft が提供する統合型 API プラットフォームであり、Office 365や Azure などの Microsoft のクラウドサービスにアクセスするための統一されたエンドポイントを提供します。
使用することで、ユーザー、グループ、ファイル、カレンダーなどのデータや機能に対して、一元的かつセキュアなアクセスが可能です。

ファイル一覧を取得

ルートディレクトリからのファイル一覧を取得します。
実装例を下記に示します。動作するために作ったため、体裁はこだわっていませんのでメモとして、、、

	try
	{
	    DelegateAuthenticationProvider prov = new DelegateAuthenticationProvider(
	(requestMessage) =>
	{
	    // サインインで使用したアクセストークンを用いてリクエストの認証ヘッダーを作成
	    requestMessage.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("bearer", _authResult.AccessToken);
	    return Task.FromResult(0);
	}
	);
	    GraphServiceClient client = new GraphServiceClient(prov);

	    IDriveItemChildrenCollectionPage items = null;
	    // 初回はルートディレクトリを指定
	    if (!_isRepeat)
	    {
	        items = await client.Me.Drive.Root.Children.Request().GetAsync();
	    }

	    // 2回目以降はルートの子ディレクトリを指定
	    else
	    {
	        items = await client.Me.Drive.Items[_dirId].Children.Request().GetAsync();
	    }
	    // 取得したファイルパス・ファイル名をリストに追加
	    foreach (DriveItem item in items)
	    {
	        // ファイル
	        if (item.Folder == null)
	        {
		string filePath = item.ParentReference.Path + "/" + item.Name;
		_fileList.Add(filePath);
	        }

	        // フォルダ
	        else
	        {
		_dirId = item.Id;
		_isRepeat = true;
		// 再帰的にフォルダを検索
		await ファイル一覧を取得処理;
	        }
	    }
	}
	catch (Exception ex)
	{
	}

指定テキストファイルの本文を取得

実装例を下記に示します。動作するために作ったため、体裁はこだわっていませんのでメモとして、、、

	try
	{
	    DelegateAuthenticationProvider prov = new DelegateAuthenticationProvider(
	(requestMessage) =>
	{
	    requestMessage.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("bearer", _authResult.AccessToken);
	    return Task.FromResult(0);
	}
	);
	    GraphServiceClient client = new GraphServiceClient(prov);

	    // ファイルを取得
	    using (var stream = await client.Me.Drive.Root.ItemWithPath(SEARCH_FILE_PATH).Content.Request().GetAsync())
	    {
	        ;
	        using (var reader = new StreamReader(stream))
	        {
		string content = null;
		content = reader.ReadToEnd();
	        }
	    }
	}
	// 指定したパスが間違っていると発生
	catch (Microsoft.Graph.ServiceException ex)
	{
	}
	catch (Exception ex)
	{
	}

指定ファイルプロパティを取得

ファイル作成日、ファイル更新日については Microsoft.Graph.DriveItem クラスを用いると適切な値が取得できました。
実装例を下記に示します。動作するために作ったため、体裁はこだわっていませんのでメモとして、、、

	DelegateAuthenticationProvider prov = new DelegateAuthenticationProvider(
        (requestMessage) =>
        {
	requestMessage.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("bearer", _authResult.AccessToken);
	return Task.FromResult(0);
        }
        );
	GraphServiceClient client = new GraphServiceClient(prov);

	// ファイルを取得
	using (var stream = await client.Me.Drive.Root.ItemWithPath(SEARCH_FILE_PATH).Content.Request().GetAsync())
	{
	    ;
	    var fileInfo = new FileInfo(SEARCH_FILE_PATH);

	    string fileName = string.Empty;
	    fileName = Path.GetFileName(SEARCH_FILE_PATH);

	    decimal fileSize = Math.Ceiling(stream.Length / 1024m);

	    // File.GetCreationTime メソッドや FileInfo.CreationTime プロパティを指定すると
	    // 初期値?(1601/01/019:00:00)が指定される
	    // DriveItem を取得すると適切なファイル作成日、更新日が取得できる
	    var driveItem = await client.Me.Drive.Root.ItemWithPath(SEARCH_FILE_PATH).Request().GetAsync();
	    DateTimeOffset? createdDateTime = driveItem.CreatedDateTime;// ファイル作成日
	    DateTimeOffset? lastWriteTimeString = driveItem.LastModifiedDateTime;// ファイル更新日
	}

Discussion