3

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.

4 Answers 4

7

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.

Sign up to request clarification or add additional context in comments.

5 Comments

@QuestionPS1991 Thank you for the clear instructions. What kind of variable is resultDriveItem? Should that be a HttpResponseMessage?
@QuestionPS1991 Your solution worked as well! Thanks so much for your help. I now face another challenge of getting the Stream working. Until now (in both solutions) the Memorystream remains empty (0 bytes). As I get no error, everything 'works' but no result.
The type of the result is DriveItem.
As for the stream, the IFormFile.OpenReadStream just opens the stream, you still need to read the file. This should get you started: link
I have solved it. I will add the solution to this discucssion. Thanks so much
3

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

Comments

1

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 
    }
}

Comments

1

I also got the 400 after trying to create a file,

after self-check I relized that the file name contained :, which is invalid for file name.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.