Falls ich es wieder suche, in Abwandlung von Modern Authentication in SharePoint Online with MSAL and CSOM (C#) – edvaldo b. guimarães filho (funktioniert nur nicht (mehr) 1:1 deswegen dieser Beitrag (s.u.)).
Voraussetzung: Enterprise App Registration im Tenant/Entra
- API-Permissions:
- Microsoft Graph: Delegated Permission/User.Read (für Sign On und Profil)
- Sharepoint: Delegated Permission/AllSites.Read (um in Sites für die der User berechtigt ist lesen zu können)
- Redirect URI: Mobile and desktop applications aka ‚Public client’/msal<applicationId>://auth (MSAL only)
Code um eine Datei aus SPO herunterzuladen:
using Microsoft.Identity.Client;
using Microsoft.SharePoint.Client;
using System;
using System.Linq;
using System.Threading.Tasks;
namespace SPOAuthMitMSAL
{
public class AuthService
{
private static IPublicClientApplication _app;
private static AuthenticationResult _authResult;
private static readonly string[] scopes = new[] { "https://mytenant.sharepoint.com/AllSites.Read" };
private static readonly string tenantId = "<tenantUUID>";
private static readonly string clientId = "<EnterpriseAppUUID>";
/// <summary>
/// Retrieves an access token, using cache or interactive login if required.
/// </summary>
public static async Task<string> GetAccessTokenAsync()
{
// Build the MSAL app instance (once)
if (_app == null)
{
Console.WriteLine($"Creating PublicClientApplicationBuilder for Tenant {tenantId} and Client {clientId}");
_app = PublicClientApplicationBuilder.Create(clientId).WithTenantId(tenantId).WithRedirectUri($"msal{clientId}://auth").Build();
}
if (_authResult != null && _authResult.ExpiresOn > DateTimeOffset.UtcNow.AddMinutes(5))
{
Console.WriteLine("Using pre existing token");
return _authResult.AccessToken;
}
try
{
Console.WriteLine("Enumerating cached accounts and use first if any for silent token acquisition");
var accounts = await _app.GetAccountsAsync();
_authResult = await _app.AcquireTokenSilent(scopes, accounts.FirstOrDefault()).ExecuteAsync();
}
catch (MsalUiRequiredException)
{
try
{
Console.WriteLine("No cached accounts, starting interactive token acquisistion");
_authResult = Task.Run(async () => await _app.AcquireTokenInteractive(scopes).WithPrompt(Prompt.SelectAccount).ExecuteAsync()).Result;
}
catch (Exception oExc)
{
Console.WriteLine($"AcquireTokenInteractive/WithPrompt failed: {oExc.Message}");
throw oExc;
}
}
Console.WriteLine($"Acquired access token successfully");
return _authResult.AccessToken;
}
/// <summary>
/// Creates a ClientContext with the valid access token automatically injected.
/// </summary>
public static ClientContext GetContext(string siteUrl)
{
var ctx = new ClientContext(siteUrl);
ctx.ExecutingWebRequest += async (sender, e) =>
{
string token = await GetAccessTokenAsync();
e.WebRequestExecutor.RequestHeaders["Authorization"] = "Bearer " + token;
};
return ctx;
}
public static void Main(string[] args)
{
Console.WriteLine("Creating context for site");
ClientContext oContext = GetContext("https://mytenant.sharepoint.com/sites/MySite");
Console.WriteLine("Load file metadata");
File oFile = oContext.Web.GetFileByServerRelativeUrl("/sites/MySite/MyList/MyDoc.pdf");
oContext.Load(oFile);
oContext.ExecuteQuery();
Console.WriteLine("Getting file stream");
ClientResult<System.IO.Stream> oSPOStream = oFile.OpenBinaryStream();
oContext.ExecuteQuery();
System.IO.Stream oStream = oSPOStream.Value;
Console.WriteLine($"Got stream, transfering {oStream.Length} bytes to local file");
byte[] oBuffer = new byte[oStream.Length];
oStream.Read(oBuffer, 0, (int)oStream.Length);
System.IO.File.WriteAllBytes($@"{Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments)}\MyDoc.pdf", oBuffer);
Console.WriteLine($"Download to '{Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments)}\\MyDoc.pdf' successfull");
}
}
}
Erklärung:
- Im Grunde läuft die Sache wie im on premises Sharepoint wenn mit CSOM gearbeitet wird ab.
- Bei der Erzeugung des ClientContext wird lediglich ein EventHandler im ClientContext hinzugefügt der jedes Mal wenn ein Request in Richtung SPO rausgeht den Authorization Header mit einem Bearer Token einfügt.
- Damit man in SPO zu einem Token kommt muss man (in MSAL)
- eine PublicApplication mit dem PublicApplicationBuilder erzeugen (und cachen weil ja vom SPO Eventhandler aufgerufen wird)
- Token akquirieren (silent oder interaktiv) und cachen (wegen Eventhandler)
Hinweise:
- In SPO funktioniert OpenBinaryDirect() nicht mehr (wirft Fehler), mit dem etwas umständlicheren OpenBinaryStream() kommt man aber genauso zum Ziel.
- Delegated access = access on behalf of a user – d.h. die App permissions (AllSites.Read) werden mit den Permissions des Users auf die tatsächliche Site „verschnitten“, vergleichbar mit Share vs. Directoryberechtigungen. AllSites.Read heißt also NICHT dass man tatsächlich auch auf allen Sites lesen kann 😀 [Erklärung bei MS]
- Consent – bei der ersten Authentifizierung in der App wird der Benutzer nach Consent befragt ob er den delegated Access auch zulassen möchte (sofern beim entsprechenden Recht nicht „Yes“ in der Spalte „Admin consent required“ steht – weil dann muss völlig überraschend ein Tenant Admin einmalig Consent erteilen). [Erklärung bei MS]
Zu den Änderungen gegenüber dem Original:
- Die Geschichte mit dem WithRedirectUri(„http://localhost“) hat erzwungen dass in der Entra App auch diese Redirect URI steht und irgendwie hat das bei mir nicht funktioniert. Mit der Umstellung auf die
msal<clientId>://authURI funktionierts. - Wenn das
AcquireTokenInteractive()asynchron läuft ist bei mir nur kurz das Loginfenster aufgesprungen und ist sofort wieder verschwunden – viaTask.Run()synchron ausgeführt läufts einwandfrei. Würde mal darauf tippen dass es am falschen Thread läuft sonst.