.NET MAUIでOneDriveにアクセスする認証方法:MSALとMicrosoft Graphでアップロード/ダウンロードを実装

OneDrive に保存したファイルを .NET MAUI からアップロード/ダウンロードしたいのに、古いサンプルの DelegateAuthenticationProvider が見つからず詰まる――そんなときは認証部分を「MSAL でトークン取得 → Microsoft Graph」に置き換えるのが最短ルートです。この記事では MAUI 8/9 を前提に、アプリ登録から実装例、仕事用/個人用アカウントの両対応、ハマりやすいポイントまでまとめます。

目次

結論:.NET MAUI から OneDrive を触るなら「MSAL(ユーザーサインイン)+ Microsoft Graph」が現行の王道

.NET MAUI アプリから個人の OneDrive(Microsoft アカウント)や職場/学校の OneDrive(組織アカウント)へアクセスして、特定フォルダーに対してアップロード/ダウンロードを行う場合、実装の軸は次の 2 点に絞れます。

  • 認証:MSAL(Microsoft Authentication Library)でユーザーにサインインしてもらい、アクセストークンを取得する
  • API 呼び出し:取得したトークンで Microsoft Graph(OneDrive / DriveItem API)を呼ぶ

つまり、「トークン取得(MSAL)と、OneDrive 操作(Graph)」を分離して考えると迷いません。Graph SDK を使う/使わないは後から選べますが、認証の中心は MSAL が基本です。

なぜ DelegateAuthenticationProvider が見つからないのか(MAUI に移植するとビルドエラーになる理由)

古い OneDrive サンプルや Xamarin 時代の Graph チュートリアルでは、HTTP リクエストに Authorization ヘッダーを差し込むために DelegateAuthenticationProvider を使う構成が多く見られます。しかし、現在の Microsoft Graph .NET SDK(5.x 系)では、SDK 自体の設計が大きく変わり、古い書き方をそのまま持ち込むと「クラスが存在しない」「Request() がない」「名前空間が違う」などのズレが発生しがちです。

SDK 側が大きく変わった背景として、Graph .NET SDK 5.x は Kiota によるコード生成へ移行し、破壊的変更が入ったことが公式パッケージ情報として明記されています。結果として、過去のサンプルの “認証の差し込み口” がそのままでは使えなくなります。

観点古いサンプルで多い形今の考え方(MAUI で安全に運用しやすい形)
認証の責務SDK 内に “ヘッダー注入” を直接書きがちMSAL でトークン取得を独立サービス化し、Graph 呼び出し側は「トークンを使うだけ」にする
SDK の前提Graph SDK の旧 API(例:Request() など)に依存Graph SDK 5.x の流儀(Kiota の IAuthenticationProvider / IAccessTokenProvider など)に合わせる
MAUI への移植性Xamarin 前提の実装や拡張が混ざりやすいMAUI 公式サンプルの設計(ATS→ATI の標準フロー、Platform 別設定)に寄せる

ポイントは「DelegateAuthenticationProvider を探して復活させる」より、“現行の差し込み口” へ置き換えるほうが、後々の保守が圧倒的に楽になることです。

最小構成で迷わないための設計(個人 OneDrive/職場 OneDrive を両対応する前提)

質問の状況(個人アカウントだけでよい、または仕事用/個人用を両方想定)を踏まえると、MAUI 側の設計は次の選択が最もシンプルです。

やりたいことおすすめ理由
ユーザーの OneDrive にアップロード/ダウンロードDelegated 権限(ユーザー委任)+ MSAL(Public client)クライアントアプリ(MAUI)は「ユーザーのサインイン」が自然。Client Secret も不要。
個人アカウントと組織アカウントを両方許可アプリ登録:組織+個人を許可、Authority:common(または AzureAdAndPersonalMicrosoftAccount)サインイン画面でユーザーがアカウントを選べる。
Graph SDK の破壊的変更に振り回されたくないGraph SDK ではなく REST(HttpClient)で呼ぶAPI 仕様(/me/drive/…)は安定。SDK のメジャー差分の影響が小さい。
コード量を減らしたい/モデルが欲しいGraph SDK 5.x を使う(ただし Kiota の認証モデルに合わせる)リクエスト組み立て、型、エラー等が扱いやすい。

この記事では「まず動く」ことを最優先に、REST(HttpClient)で OneDrive を操作する実装例を中心に、Graph SDK 5.x を使いたい人向けに Kiota 認証プロバイダーの考え方も補足します。

アプリ登録(Entra ID)で必ず押さえる設定:既存のアプリ ID を流用して OK

すでに Azure AD(現:Microsoft Entra ID)でアプリ登録済みのアプリケーション ID があるなら、基本的にそのまま流用できます。重要なのは、MAUI のサインイン方式(Public client)に合わせた設定を揃えることです。

サポートするアカウントの種類(仕事用/個人用の両方を想定する場合)

仕事用/個人用 OneDrive の両方を想定するなら、アプリ登録の「サポートされているアカウントの種類」は “任意の組織ディレクトリと個人用 Microsoft アカウント” を選びます。MAUI 向けのチュートリアルでも、この前提でアプリを作成する流れになっています。

リダイレクト URI(MAUI の基本):msal{client_id}://auth

.NET MAUI のサインイン準備(Microsoft Learn の手順)では、Mobile and desktop applications のプラットフォーム構成で msal{client_id}://auth を追加するよう案内されています。ここがズレると “redirect_uri が無効” 系で必ず詰まります。

なお、MSAL の wiki でも Public client の既定リダイレクト URI として msal{ClientId}://auth が挙げられており、基本方針は一致しています(プラットフォームやブローカー利用時は別形式に変わる場合あり)。

OneDrive 操作用の権限(スコープ)選び:最小権限から始める

OneDrive のアップロード/ダウンロードをする最小の Delegated 権限は、用途により以下のように選びます。

やりたい操作推奨スコープ例(Delegated)補足
ダウンロード(読み取り)Files.Read/content のダウンロードに必要。
アップロード/更新(書き込み)Files.ReadWrite小さいファイルは PUT 1 回で可能(上限あり)。
大きいファイルのアップロードFiles.ReadWrite(+アップロードセッション)アップロードセッション+分割アップロードで実装。
アプリ専用領域だけ使いたいFiles.ReadWrite.AppFolderユーザーの “全ファイル” に触れない設計にできる(最小権限の考え方として強い)。

注意:「特定フォルダーだけに権限を絞りたい」という要件は、Delegated の Files.ReadWrite だけでは “技術的にフォルダー単位で完全制限” できません。ユーザーが許可した範囲は基本的に OneDrive 全体になりやすいので、気になる場合は AppFolder(アプリ専用フォルダー)を第一候補にすると安全です。

.NET MAUI 側の実装:MSAL でアクセストークンを取る(ATS → ATI の標準フロー)

MAUI における MSAL の基本は、次の流れです。

  1. まずはキャッシュから静かに取得(Acquire Token Silent / ATS)
  2. 無理なら UI を出して取得(Acquire Token Interactive / ATI)

この「ATS → ATI」は、MAUI 向け MSAL サンプルでも標準フローとして説明されています。

前提:Public client(モバイル/デスクトップ)なので Client Secret は使わない

MAUI アプリは配布物から逆コンパイルされ得るため、Client Secret を埋め込む構成は避けます。MSAL の Public client で PKCE を使ったサインインを基本にします(=シークレット不要)。

必要な NuGet(例)

  • Microsoft.Identity.Client(MSAL)
  • (Graph SDK を使うなら)Microsoft.Graph

Graph 側は TokenCredential(Azure.Identity)にも対応しますが、ユーザーサインインが必要な MAUI では MSAL を中心に据えるのが現実的です。

実装例:MSAL のトークン取得サービス(最小構成)

まずは「Graph を呼べるアクセストークンを返す」だけのサービスを作ります。UI でログインが必要になったら、AcquireTokenInteractive を実行します。

using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Identity.Client;

public sealed class MsalTokenService
{
    private readonly IPublicClientApplication _pca;
    private readonly string[] _scopes;

    public MsalTokenService(string clientId, string[] scopes)
    {
        _scopes = scopes;

        // 両対応(仕事用/個人用)を狙うなら Authority は common 相当でOK。
        // 既定でも common になることが多いですが、明示したほうが混乱しづらいです。
        _pca = PublicClientApplicationBuilder
            .Create(clientId)
            // 例:共通エンドポイント(work/school + personal)
            .WithAuthority(AadAuthorityAudience.AzureAdAndPersonalMicrosoftAccount)
            // MAUI チュートリアルで案内される形式。ポータル側にも同じ URI を登録する。
            .WithRedirectUri($"msal{clientId}://auth")
            .Build();
    }

    public async Task<string> GetAccessTokenAsync(CancellationToken ct = default)
    {
        var accounts = await _pca.GetAccountsAsync().ConfigureAwait(false);
        var first = accounts.FirstOrDefault();

        try
        {
            var silent = await _pca
                .AcquireTokenSilent(_scopes, first)
                .ExecuteAsync(ct)
                .ConfigureAwait(false);

            return silent.AccessToken;
        }
        catch (MsalUiRequiredException)
        {
            // UI が必要:ここでブラウザが開く
            var interactive = await _pca
                .AcquireTokenInteractive(_scopes)
                // プラットフォームごとに親ウィンドウ/Activity の渡し方が必要になるケースあり
                // (後述:Platforms 配下の設定や公式サンプル参照)
                .ExecuteAsync(ct)
                .ConfigureAwait(false);

            return interactive.AccessToken;
        }
    }

    public async Task SignOutAsync()
    {
        var accounts = await _pca.GetAccountsAsync().ConfigureAwait(false);
        foreach (var acc in accounts)
        {
            await _pca.RemoveAsync(acc).ConfigureAwait(false);
        }
    }
}

上記は概念を掴むための最小形です。実運用では、Android/iOS/Windows の “親ウィンドウ” 周り(WithParentActivityOrWindow など)やブローカー利用の有無で調整が必要になることがあります。MAUI 向け MSAL サンプルは、Platforms 配下にプラットフォーム固有コードを分離し、Wrapper(Singleton)で UI と認証を分ける構成が紹介されています。

OneDrive へのアップロード/ダウンロード:まずは REST(HttpClient)で “確実に動く” を作る

「認証はできたが、SDK でまた詰まる」を避けるため、まずは Graph REST を叩く方法を紹介します。Graph SDK を使う場合でも、最終的に送っているのは HTTP なので、REST で動く形を作っておくとデバッグが楽になります。

OneDrive の基本:Drive / DriveItem と “パス指定” の考え方

Microsoft Graph では OneDrive のファイルやフォルダーは DriveItem として扱われ、/me/drive を起点にパス指定(root:/path/to/file)で参照できます。OneDrive / OneDrive for Business / SharePoint ドキュメントライブラリでも同じ API 体系で扱えるのが強みです。

アップロード(小さいファイル):PUT /me/drive/root:/フォルダー/ファイル:/content

小さいファイル(単発 PUT で送れるサイズ)なら、PUT /content でアップロードできます。この方法は 最大 250MB までという上限が明記されています。

using System;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;

public sealed class OneDriveRestClient
{
    private readonly HttpClient _http;
    private readonly Func<CancellationToken, Task<string>> _getToken;

    public OneDriveRestClient(HttpClient http, Func<CancellationToken, Task<string>> getToken)
    {
        _http = http;
        _getToken = getToken;
    }

    // folderPath 例: "MyAppData" or "MyAppData/Sub"
    // fileName   例: "sample.txt"
    public async Task UploadSmallFileAsync(string folderPath, string fileName, Stream content, CancellationToken ct = default)
    {
        var token = await _getToken(ct).ConfigureAwait(false);

        // パスは「/」区切りを保ったまま URL エンコードしたいので、セグメント単位で Escape するのが安全
        string EncodePath(string path)
        {
            var parts = path.Replace("\\", "/").Trim('/').Split('/', StringSplitOptions.RemoveEmptyEntries);
            return string.Join("/", Array.ConvertAll(parts, Uri.EscapeDataString));
        }

        var encodedFolder = EncodePath(folderPath);
        var encodedFile = Uri.EscapeDataString(fileName);

        // 例: https://graph.microsoft.com/v1.0/me/drive/root:/FolderA/FileB.txt:/content
        var url = $"https://graph.microsoft.com/v1.0/me/drive/root:/{encodedFolder}/{encodedFile}:/content";

        using var req = new HttpRequestMessage(HttpMethod.Put, url);
        req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
        req.Content = new StreamContent(content);
        req.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");

        using var res = await _http.SendAsync(req, ct).ConfigureAwait(false);
        res.EnsureSuccessStatusCode();
    }
}

アップロード先を「特定フォルダー」に固定したい場合は、アプリ側で folderPath を固定し、ユーザー入力を受け取らない(または許可したプレフィックス配下だけに正規化する)ようにすると事故を減らせます。

アップロード(大きいファイル):アップロードセッション(分割アップロード)を使う

250MB を超える可能性がある場合や、通信が切れやすいモバイル回線で確実性を上げたい場合は、アップロードセッション(resumable upload)に切り替えます。Graph SDK には分割アップロードを面倒見てくれる LargeFileUploadTask が用意されており、OneDrive 例も公開されています。

REST でも実装できますが、「まず動かす」段階では SDK の LargeFileUploadTask を使うほうが工数を減らせます(後述の SDK セクション参照)。

ダウンロード:GET /content は 302 リダイレクトが返るのが仕様

OneDrive のダウンロードは GET /content を使いますが、レスポンスとして 302 Found で一時的なダウンロード URL にリダイレクトされるのが仕様です。多くの HTTP クライアントは自動追従してくれます。さらに、そのダウンロード URL は短時間で期限切れになり得るため「すぐ使う」が基本です。

using System;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;

public sealed partial class OneDriveRestClient
{
    // itemPath 例: "MyAppData/sample.txt"(root からの相対)
    public async Task DownloadFileAsync(string itemPath, Stream destination, CancellationToken ct = default)
    {
        var token = await _getToken(ct).ConfigureAwait(false);

        string EncodePath(string path)
        {
            var parts = path.Replace("\\", "/").Trim('/').Split('/', StringSplitOptions.RemoveEmptyEntries);
            return string.Join("/", Array.ConvertAll(parts, Uri.EscapeDataString));
        }

        var encoded = EncodePath(itemPath);
        var url = $"https://graph.microsoft.com/v1.0/me/drive/root:/{encoded}:/content";

        using var req = new HttpRequestMessage(HttpMethod.Get, url);
        req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);

        // ストリーミングで受け取りたいので ResponseHeadersRead
        using var res = await _http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, ct).ConfigureAwait(false);
        res.EnsureSuccessStatusCode();

        await using var stream = await res.Content.ReadAsStreamAsync(ct).ConfigureAwait(false);
        await stream.CopyToAsync(destination, ct).ConfigureAwait(false);
    }
}

モバイル端末ではメモリ制約が厳しいことがあるため、上記のようにストリーミング(ResponseHeadersRead)で受け、保存先も Stream として渡す設計にしておくと堅牢です。

Microsoft Graph SDK 5.x を使う場合:Kiota の AuthenticationProvider に合わせる

Graph SDK を使う場合の最大のポイントは、「SDK のリクエスト送信前に認証情報を足す仕組み」が Kiota のモデル(IAuthenticationProvider / IAccessTokenProvider)に寄っていることです。

Kiota の認証ドキュメントでは、認証プロバイダーのインターフェース(IAuthenticationProvider)と、アクセストークン供給用(IAccessTokenProvider)、さらに Bearer トークンを Authorization ヘッダーに付与する BaseBearerTokenAuthenticationProvider の存在が説明されています。

実装方針(おすすめ):MSAL でトークン取得 → IAccessTokenProvider で渡す

MAUI の場合、トークン取得には UI が絡むため、IAccessTokenProvider の中で毎回 UI を出す設計は避け、次のどちらかに寄せると運用が安定します。

  • 方式A(堅実):画面の「サインイン」ボタンで MSAL の ATI を完了させ、以降は IAccessTokenProvider は ATS(サイレント)だけ行う
  • 方式B(最小依存):Graph SDK を使わず、前章の REST(HttpClient)で呼ぶ

方式A の “骨格” は次のようになります(概念例)。

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Kiota.Abstractions.Authentication;

public sealed class MsalAccessTokenProvider : IAccessTokenProvider
{
    private readonly Func<CancellationToken, Task<string>> _getTokenSilently;

    public AllowedHostsValidator AllowedHostsValidator { get; } = new AllowedHostsValidator();

    public MsalAccessTokenProvider(Func<CancellationToken, Task<string>> getTokenSilently)
    {
        _getTokenSilently = getTokenSilently;
        // Graph にだけトークンを渡す(不要なホストに渡さない)
        AllowedHostsValidator.AllowedHosts.Add("graph.microsoft.com");
    }

    public Task<string> GetAuthorizationTokenAsync(
        Uri uri,
        Dictionary<string, object>? additionalAuthenticationContext = null,
        CancellationToken cancellationToken = default)
    {
        return _getTokenSilently(cancellationToken);
    }
}

// 使う側(概念)
// var tokenProvider = new MsalAccessTokenProvider(ct => tokenService.GetAccessTokenSilentlyAsync(ct));
// var authProvider  = new BaseBearerTokenAuthenticationProvider(tokenProvider);
// var graphClient   = new GraphServiceClient(requestAdapter); // SDK の初期化はバージョンにより形が変わる

Graph SDK の初期化(RequestAdapter 周り)はバージョンで差が出るため、まずは REST で要件を満たし、SDK は必要になってから導入するのが安全です。「SDK で得たいメリット(モデル、ページング、アップロードタスク等)」が明確なら、そこで初めて SDK を使うと投資対効果が良くなります。

仕事用/個人用アカウントの両対応:app registration と Authority の “両方” を揃える

両対応の要点は、次の 2 点を同時に満たすことです。

  • アプリ登録:組織+個人を許可(supported account types)
  • MSAL:Authority を common 相当にする(AzureAdAndPersonalMicrosoftAccount など)

MAUI のサインイン手順でも、アプリ登録を「任意の組織ディレクトリ+個人アカウント」で作り、リダイレクト URI を msal{client_id}://auth にする流れが示されています。

また、MSAL の wiki では audience として organizations / consumers / common(コード上は AadAuthorityAudience など)を使い分けられることが説明されています。要件が「両方」なら common(AzureAdAndPersonalMicrosoftAccount)が最も迷いません。

アカウント切り替え UI の作り方(実務で効く小ワザ)

  • 「サインイン」ボタンの他に「アカウント切り替え」を用意し、GetAccountsAsync() の結果から選べるようにする
  • トラブル時の切り分け用に「サインアウト(キャッシュ削除)」を必ず用意する
  • 組織アカウントは条件付きアクセス等でブローカーが必要になることがあるため、企業端末向けには Broker 版サンプルも検討する

ブローカー(Microsoft Authenticator 等)を使う構成は、SSO や条件付きアクセス対応に有効で、MSAL 側の手順やリダイレクト URI 形式が追加で必要になる点が整理されています。

よくあるハマりポイント(MAUI × MSAL × OneDrive/Graph)と対処

症状よくある原因対処
DelegateAuthenticationProvider が見つからないGraph SDK のメジャーバージョン差(5.x は Kiota ベース)MSAL でトークン取得し、REST か Kiota の認証プロバイダー(IAuthenticationProvider/IAccessTokenProvider)へ寄せる
redirect_uri が無効(invalid_request など)ポータルとアプリのリダイレクト URI が不一致Mobile and desktop applications に msal{client_id}://auth を登録し、コード側も合わせる
アップロードが 4xx/5xx で失敗する権限不足/サイズ制限/アップロード方法不一致小さいファイルは PUT /content(上限あり)、大きい場合はアップロードセッションへ切替
ダウンロードで想定と違う挙動/content は 302 リダイレクトが仕様HTTP クライアントが Location を追従できるか確認。ストリーミング受信でメモリ節約
MsalUiRequiredException が頻発する初回サインイン未実施/キャッシュに該当アカウント無し/ポリシーで再認証要求ATS→ATI の標準フローに従い、必要時だけインタラクティブを起動する

安全に運用するためのチェックリスト(最小権限・トークン扱い・ログ)

  • 最小権限:まずは Files.Read / Files.ReadWrite で開始し、必要になってから拡張する(.All を安易に付けない)
  • フォルダーを絞りたい:可能なら AppFolder を採用して “アプリ専用領域” に寄せる
  • トークンの保存:アクセストークン文字列を自前で永続化しない(MSAL のキャッシュ運用に任せる)
  • 失敗時導線:サインアウト(アカウント削除)と再サインインを UI で提供する
  • ログ:まずは MSAL/HTTP のエラー内容を収集できるようにし、端末依存の問題(リダイレクト、ブローカー、条件付きアクセス)に備える

まとめ:古いサンプルを追いかけるより「MSAL+Graph」で組み直すのが最短

  • DelegateAuthenticationProvider ベースの古いサンプルは、そのまま MAUI に移植しようとすると詰まりやすい
  • MAUI の現行ルートは、MSAL(Public client)でトークンを取り、Microsoft Graph で OneDrive を操作する
  • 最初は REST(HttpClient)でアップロード/ダウンロードを確実に動かし、必要になったら Graph SDK 5.x(Kiota 認証)へ拡張
  • 仕事用/個人用の両対応は「アプリ登録のアカウント種別」と「MSAL Authority」を揃える

この形に寄せるだけで、OneDrive の特定フォルダーへのアップロード/ダウンロードは実装でき、さらに MAUI のバージョンアップや Graph SDK の更新にも追従しやすくなります。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次