1
votes

I get an error 400 Bad Request when I try to upload a file to my OneDrive with a daemon app, using the Microsoft Graph API. I use a HttpClient, not a GraphServiceClient as the latter assumes interaction and works with a DelegatedAuthenticationProvider(?).

  • The App is registered in AAD and has the right Application Permission (Microsoft Graph / File ReadWrite.All)
  • The registration is for One Tenant and has no redirect url (as per instructions)

The main Method Upload gets an AccessToken through a Helper AuthenticationConfig and puts a file to OneDrive/SharePoint with the Helper ProtectedApiCallHelper.

[HttpPost]
    public async Task<IActionResult> Upload(IFormFile file)
    {            
        var toegang = new AuthenticationConfig();
        var token = toegang.GetAccessTokenAsync().GetAwaiter().GetResult();

        var httpClient = new HttpClient();
        string bestandsnaam = file.FileName;
        var serviceEndPoint = "https://graph.microsoft.com/v1.0/drive/items/{Id_Of_Specific_Folder}/";            

        var wurl = serviceEndPoint + bestandsnaam + "/content";
// The variable wurl looks as follows: "https://graph.microsoft.com/v1.0/drive/items/{Id_Of_Specific_Folder}/proefdocument.txt/content"
        var apicaller = new ProtectedApiCallHelper(httpClient);
        apicaller.PostWebApi(wurl, token.AccessToken, file).GetAwaiter();

        return View();
    }

I get a proper Access Token using the following standard helper AuthenticationConfig.GetAccessToken()

public async Task<AuthenticationResult> GetAccessTokenAsync()
    {
        AuthenticationConfig config = AuthenticationConfig.ReadFromJsonFile("appsettings.json");            
        IConfidentialClientApplication app;

        app = ConfidentialClientApplicationBuilder.Create(config.ClientId)
            .WithClientSecret(config.ClientSecret)
            .WithAuthority(new Uri(config.Authority))
            .Build();

        string[] scopes = new string[] { "https://graph.microsoft.com/.default" };

        AuthenticationResult result = null;
        try
        {
            result = await app.AcquireTokenForClient(scopes).ExecuteAsync();
            return result;
        }
        catch (MsalServiceException ex) when (ex.Message.Contains("AADSTS70011"))
        {
            ...
            return result;
        }
    }

With the AccessToken, the Graph-Url and the File to be uploaded (as an IFormFile) the Helper ProtectedApiCallHelper.PostWebApi is called

public async Task PostWebApi(string webApiUrl, string accessToken, IFormFile fileToUpload)
    {
        Stream stream = fileToUpload.OpenReadStream();
        var x = stream.Length;
        HttpContent content = new StreamContent(stream);

        if (!string.IsNullOrEmpty(accessToken))
        {
            var defaultRequestHeaders = HttpClient.DefaultRequestHeaders;               
            HttpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/octet-stream"));             
            defaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", accessToken);

// Here the 400 Bad Request happens
            HttpResponseMessage response = await HttpClient.PutAsync(webApiUrl, content);

            if (response.IsSuccessStatusCode)
            {
                return;
            }
            else
            {
                 //error handling                   
                return;
            }
        }
    }

EDIT

Please see the working solution below.

3

3 Answers

4
votes

You can use GraphServiceClient without user interaction using a client id and a client secret. First, create a class called GraphAuthProvider:

    public class GraphAuthProvider
{
    public async Task<GraphServiceClient> AuthenticateViaAppIdAndSecret(
        string tenantId,
        string clientId, 
        string clientSecret)
    {
        var scopes = new string[] { "https://graph.microsoft.com/.default" };

        // Configure the MSAL client as a confidential client
        var confidentialClient = ConfidentialClientApplicationBuilder
            .Create(clientId)
            .WithAuthority($"https://login.microsoftonline.com/{tenantId}/v2.0")
            .WithClientSecret(clientSecret)
            .Build();

        // Build the Microsoft Graph client. As the authentication provider, set an async lambda
        // which uses the MSAL client to obtain an app-only access token to Microsoft Graph,
        // and inserts this access token in the Authorization header of each API request. 
        GraphServiceClient graphServiceClient =
            new GraphServiceClient(new DelegateAuthenticationProvider(async (requestMessage) =>
            {

                // Retrieve an access token for Microsoft Graph (gets a fresh token if needed).
                var authResult = await confidentialClient
                    .AcquireTokenForClient(scopes)
                    .ExecuteAsync();

                // Add the access token in the Authorization header of the API request.
                requestMessage.Headers.Authorization =
                    new AuthenticationHeaderValue("Bearer", authResult.AccessToken);
            })
        );

        return graphServiceClient;
    }
}

You can then create authenticated GraphServiceClients and use them to upload files, for example to SharePoint:

        GraphServiceClient _graphServiceClient = await _graphAuthProvider.AuthenticateViaAppIdAndSecret(
            tenantId,
            clientId,
            appSecret);


        using (Stream fileStream = new FileStream(
            fileLocation,
            FileMode.Open,
            FileAccess.Read))
        {
            resultDriveItem = await _graphServiceClient.Sites[sites[0]]
                    .Drives[driveId].Root.ItemWithPath(fileName).Content.Request().PutAsync<DriveItem>(fileStream);

       }

Regarding the permissions: You may need more permissions than just Files.ReadWrite.All. As far as I know, an app needs the application permission Sites.ReadWrite.All to upload documents to SharePoint.

2
votes

According to document : Upload or replace the contents of a DriveItem

If using client credential flow(M2M flow without user) , you should use below request :

PUT /drives/{drive-id}/items/{parent-id}:/{filename}:/content

Instead of :

https://graph.microsoft.com/v1.0/drive/items/{Id_Of_Specific_Folder}/proefdocument.txt/content
0
votes

This the final working example using a GraphServiceClient

public async Task<DriveItem> UploadSmallFile(IFormFile file, bool uploadToSharePoint)
    {
        IFormFile fileToUpload = file;
        Stream ms = new MemoryStream();

        using (ms = new MemoryStream()) //this keeps the stream open
        {
            await fileToUpload.CopyToAsync(ms);
            ms.Seek(0, SeekOrigin.Begin);
            var buf2 = new byte[ms.Length];
            ms.Read(buf2, 0, buf2.Length);

            ms.Position = 0; // Very important!! to set the position at the beginning of the stream
            GraphServiceClient _graphServiceClient = await AuthenticateViaAppIdAndSecret();

            DriveItem uploadedFile = null;
            if (uploadToSharePoint == true)
            {
                uploadedFile = (_graphServiceClient
                .Sites["root"]
                .Drives["{DriveId}"]
                .Items["{Id_of_Targetfolder}"]
                .ItemWithPath(fileToUpload.FileName)
                .Content.Request()
                .PutAsync<DriveItem>(ms)).Result;
            }
            else
            {
                // Upload to OneDrive (for Business)
                uploadedFile = await _graphServiceClient
                .Users["{Your_EmailAdress}"]
                .Drive
                .Root
                .ItemWithPath(fileToUpload.FileName)
                .Content.Request()
                .PutAsync<DriveItem>(ms);
            }

            ms.Dispose(); //clears memory
            return uploadedFile; //returns a DriveItem. 
        }
    }

You can use a HttpClient as well

public async Task PostWebApi(string webApiUrl, string accessToken, IFormFile fileToUpload)
    {

        //Create a Stream and convert it to a required HttpContent-stream (StreamContent).
        // Important is the using{...}. This keeps the stream open until processed
        using (MemoryStream data = new MemoryStream())
        {
            await fileToUpload.CopyToAsync(data);
            data.Seek(0, SeekOrigin.Begin);
            var buf = new byte[data.Length];
            data.Read(buf, 0, buf.Length);
            data.Position = 0;
            HttpContent content = new StreamContent(data);


            if (!string.IsNullOrEmpty(accessToken))
            {
                // NO Headers other than the AccessToken should be added. If you do
                // an Error 406 is returned (cannot process). So, no Content-Types, no Conentent-Dispositions

                var defaultRequestHeaders = HttpClient.DefaultRequestHeaders;                    
                defaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", accessToken);

                HttpResponseMessage response = await HttpClient.PutAsync(webApiUrl, content);

                if (response.IsSuccessStatusCode)
                {
                    return;
                }
                else
                {
                    // do something else
                    return;
                }
            }
            content.Dispose();
            data.Dispose();
        } //einde using memorystream 
    }
}