How can you programmatically and securely access your OneDrive data using a Microsoft Account in UWP? One of our community members recently hit a brick wall trying to do just that and she reached out to Twitter to see if there is a solution...luckily there is
Let's see what it takes to put everything together.
Prerequisites
You'll need the following if you want to configure, build and run the application:
- An Azure Active Directory (get a free one here)
- A OneDrive that has some data
- Visual Studio 2019
- Windows 10
- The UWP workload for VS2019 installed
Create the Azure AD App Registration
For our UWP app to work and allow user to authenticate with their MSA accounts, we need to configure an App Registration in Azure AD. Unlike the commonly used single tenant, multi tenant options, we will opt in for an MSA-only app registration. After all, we don't expect any organizational users to sign in to the app. Think of it as a consumer-only solution built around MSA
Head over to your Azure AD portal (aad.portal.azure.com) and navigate to the App Registrations tab. Click the Register new app and give it a Name. Under Supported account types make sure to select Personal Microsoft accounts only. And press Register
Since we will be working with OneDrive, we need to set up the right permissions in our app. Head to the API Permissions tab and press the Add a permission button and select Microsoft Graph:
In the permission selection pane, search for file
and select the following permissions:
- Files.Read
- Files.Read.All
- Files.ReadWrite
- Files.ReadWrite.All
Only add the
ReadWrite
ones if you intend to allow users to add new files and folders. Otherwise, ready-only permissions are better (least privilege principle). Make sure to press the Add permissions button at the bottom.
Finally, we need to configure the Authentication. This is where we tell our App Registration what kind of app will be used to authenticate users. Navigate to the Authentication tab and press the Add a platform. Since our app is a UWP app we need to select Mobile and Desktop Applications
In the Redirect URIs select the top option and press the Configure button.
The last step in this process is to copy the ClientID value from the Overview tab as we will need it in our code.
Implement the authentication code in UWP
The easiest way, by far, to work with OneDrive is via the MS Graph. The MS Graph is the One API to Rule them All when it comes to Microsoft data. The process is as follows
- The app signs in the user with their MSA account to Azure AD. You can implement this using MSAL or any other OIDC/OAuth2 compliant library. MSAL is my preference but you do you :)
- The app requests an access token to the Graph. First silently (if there is one in the cache) and then interactively. This is where the user also needs to consent to the OneDrive permissions
- The app uses the access token to instantiate the Graph client and call the appropriate Graph endpoint
3 steps and a few lines of code later...
First we need to add 2 NuGet packages:
- Microsoft.Graph (the Graph library)
- Microsoft.Identity.Client (our MSAL library)
Technically you don't need the Graph library and you're more than welcome to write artisan HTTP calls to the Graph endpoint. But nobody ain't got time for that so the SDK it is
With the NuGet packages in place, we got all we need to write some authentication code!
Open MainPage.xaml.cs and add the following properties:
private string[] scopes = new string[]
{
"user.read",
"Files.Read",
"Files.Read.All",
"Files.ReadWrite",
"Files.ReadWrite.All"
};
private const string ClientId = "72e74aaf-001f-4131-8db7-924bcec85a94";
private const string Tenant = "consumers";
private const string Authority = "https://login.microsoftonline.com/" + Tenant;
private static IPublicClientApplication PublicClientApp;
private static readonly string MSGraphURL = "https://graph.microsoft.com/v1.0/";
private static AuthenticationResult authResult;
NOTE: for
authority
we set ourTenant
toConsumers
since this app only works with MSA accounts :)
Now, let's create a couple of methods. The first will be for signing in the user and getting the access token we need and the second one will be to instantiate the Graph client
private static async Task<string> SignInUserAndGetTokenUsingMSAL(string[] scopes)
{
PublicClientApp = PublicClientApplicationBuilder.Create(ClientId)
.WithAuthority(Authority)
.WithUseCorporateNetwork(false)
.WithRedirectUri(DefaultRedirectUri.Value)
.WithLogging((level, message, containsPii) =>
{
Debug.WriteLine($"MSAL: {level} {message} ");
}, LogLevel.Warning, enablePiiLogging: false,
enableDefaultPlatformLogging: true)
.Build();
var accounts = await PublicClientApp.GetAccountsAsync().ConfigureAwait(false);
var firstAccount = accounts.FirstOrDefault();
try
{
authResult = await PublicClientApp.AcquireTokenSilent(scopes, firstAccount).ExecuteAsync();
}
catch (MsalUiRequiredException ex)
{
// A MsalUiRequiredException happened on AcquireTokenSilentAsync. This indicates you need to call AcquireTokenAsync to acquire a token
Debug.WriteLine($"MsalUiRequiredException: {ex.Message}");
authResult = await PublicClientApp.AcquireTokenInteractive(scopes).ExecuteAsync().ConfigureAwait(false);
}
return authResult.AccessToken;
}
private async static Task<GraphServiceClient> SignInAndInitializeGraphServiceClient(string[] scopes)
{
GraphServiceClient graphClient = new GraphServiceClient(MSGraphURL,
new DelegateAuthenticationProvider(async (requestMessage) =>{
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", await SignInUserAndGetTokenUsingMSAL(scopes));
}));
return await Task.FromResult(graphClient);
}
Finally, we need to wire up the UI to call these methods. In the button event handler, let's add the following code:
private async void CallGraphButton_Click(object sender, RoutedEventArgs e)
{
try
{
GraphServiceClient graphClient = await SignInAndInitializeGraphServiceClient(scopes);
var driveItems = await graphClient.Me.Drive.Root.Children.Request().GetAsync();
var driveDataDescription = GetDriveItemInfo(driveItems);
await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>{
ResultText.Text = driveDataDescription;
DisplayBasicTokenInfo(authResult);
this.SignOutButton.Visibility = Visibility.Visible;
});
}
catch (MsalException msalEx)
{
await DisplayMessageAsync($"Error Acquiring Token:{System.Environment.NewLine}{msalEx}");
}
catch (Exception ex)
{
await DisplayMessageAsync($"Error Acquiring Token Silently:{System.Environment.NewLine}{ex}");
return;
}
}
Here, we first grab the Graph client and then we do a call to the OneDrive endpoint for the currently signed in user. This returns a collection of DriveItems
and we have a helper method that iterates through the collection and prints out whether the item is a folder or a file. The helper method is provided below:
private string GetDriveItemInfo(IDriveItemChildrenCollectionPage driveItems)
{
var sb = new StringBuilder();
foreach (var item in driveItems)
{
if(item.Folder != null)
{
sb.AppendLine($"Folder: {item.Name} contains '{item.Folder.ChildCount}' items");
}
else
{
sb.AppendLine($"File: {item.Name} has a MimeType of '{item.File.MimeType}' and size: '{item.Size}'");
}
}
return sb.ToString();
}
Running the app should look like this:
Show me the c0d3z
If you want to get your hands on the working solution, I have a repo waiting for you on GitHub
Conclusion
MS Graph has made it significantly easier to work with OneDrive. Admittedly, getting the access token for Graph requires a bit of an effort in setting up Azure AD and writing a few lines of code for authentication, but you now have a robust, secure and scalable solution that works as designed.
If you have any questions or need support with Identity related issues, make sure to join our Discord community. Or ping us on Twitter @christosmatskas and @AzureAndChill.
Top comments (0)