Accessing web api secured via Azure Active Directory using MSAL

Accessing web api secured via Azure Active Directory using MSAL

JOfford_13
Advocate Advocate
780 Views
3 Replies
Message 1 of 4

Accessing web api secured via Azure Active Directory using MSAL

JOfford_13
Advocate
Advocate

How would one go about accessing a Azure AD protected web api using the Microsoft Identity Platform (MSAL) NuGet package? Our company's web api access (and also things like the Microsoft Graph/Teams) is controlled via Azure Active Directory therefore I need to get an access token.

 

The main concern is the pop-up window that MSAL launches when no token has been established. AquireTokenInteractive() is an async method with no sync option. The pop-up asks for the account (work/school/etc) and possibly some two-factor authentication information. Is there anything wrong with the following approach that would cause issues in Revit? It appears to be functioning in .NET Framework 4.8.

using Autodesk.Revit.Attributes;
using Autodesk.Revit.DB;
using Autodesk.Revit.UI;
using Microsoft.Identity.Client;
using System;
using System.Diagnostics;
using System.Threading.Tasks;

[Transaction(TransactionMode.Manual)]
public class MyCommand : IExternalCommand
{
    public Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements)
    {
        try
        {
            Task.Run(async () => { await RunAsync(); }).Wait();
            // do more stuff here
        }
        catch (Exception ex)
        {
            Debug.Print(ex.ToString());
        }

        return Result.Cancelled;
    }
}

public static async Task RunAsync()
{
    AuthenticationResult authResult = null;

    var app = PublicClientApplicationBuilder.Create("<clientId>")
                .WithDefaultRedirectUri()
                .WithAuthority(AzureCloudInstance.AzurePublic, "<tenantId>")
                .Build();

    var scopes = new string[] { "<scope-name>" };
    var loginHint = "<my-hint>";

    try
    {
        authResult = await app.AcquireTokenSilent(scopes, loginHint)
            .ExecuteAsync()
            .ConfigureAwait(false);
    }
    catch (MsalUiRequiredException ex)
    {
        // A MsalUiRequiredException happened on AcquireTokenSilent. 
        // This indicates you need to call AcquireTokenInteractive to acquire a token
        System.Diagnostics.Debug.WriteLine($"MsalUiRequiredException: {ex.Message}");

        try
        {
            // this will launch a pop-up window
            authResult = await app.AcquireTokenInteractive(scopes)
                .WithLoginHint(loginHint)
                .ExecuteAsync()
                .ConfigureAwait(false);
        }
        catch (MsalException msalex)
        {
            Debug.WriteLine($"Error Acquiring Token:{System.Environment.NewLine}{msalex}");
        }
    }
    catch (Exception ex)
    {
        Debug.WriteLine($"Error Acquiring Token Silently:{System.Environment.NewLine}{ex}");
        return;
    }

    if (authResult != null)
    {
        var httpClient = new System.Net.Http.HttpClient();
        System.Net.Http.HttpResponseMessage response;
        try
        {
            var request = new System.Net.Http.HttpRequestMessage(System.Net.Http.HttpMethod.Get, "https://<my-web-api-endpoint>");
            // Add the token in Authorization header
            request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", authResult.AccessToken);
            response = await httpClient.SendAsync(request).ConfigureAwait(false);
            var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
            MessageBox.Show(content, "Web Result");
        }
        catch (Exception ex)
        {
            Debug.WriteLine($"error calling api: {ex}");
        }
    }
}

 

0 Likes
781 Views
3 Replies
Replies (3)
Message 2 of 4

jeremy_tammik
Alumni
Alumni

Hard to say... running asynchronous code in an external command will obviously leave the main Revit execution thread and thus the valid Revit API context... shouldn't matter if you have no need to access the Revit API anyway in that context. 

 

Can't you make life simpler for yourself by separating the authentification code completely from your Revit add-in, either by obtaining it beforehand or running it in a completely separate process?

 

If you need to communicate it back to your add-in, you could achieve that using IPC, couldn't you?

 

And save yourself some pain?

 

https://thebuildingcoder.typepad.com/blog/2019/04/set-floor-level-and-use-ipc-for-disentanglement.ht...

  

Jeremy Tammik Developer Advocacy and Support + The Building Coder + Autodesk Developer Network + ADN Open
0 Likes
Message 3 of 4

JOfford_13
Advocate
Advocate

I haven't heard of IPC before so I'll check out the link see if that will suffice. There are two main async events that will come into play.

 

  1. Getting (and refreshing) the access token
  2. Get/Post requests to the web api and handling related errors

It'd be wonderful to move all that to a .NET core app on the side. Thanks for the suggestions.

0 Likes
Message 4 of 4

JOfford_13
Advocate
Advocate

After looking at the examples it looks challenging to deal with all the back-and-forth and Windows api calls.

 

In testing the code below it appears the ManagedThreadId is consistent before and after waiting on the task. Everything started and finished on thread 1 for me. Given this scenario is it still inappropriate to call the Revit api after the task is finished? I don't fully understand the danger if the answer is no.

 

public Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements)
{

    Debug.Print($"starting on thread {Thread.CurrentThread.ManagedThreadId}");

    try
    {
        Task.Run(async () => { await DoSomethingAsync(); }).Wait();

        // accessing the Revit api here is bad??
    }
    catch (Exception ex)
    {
        Debug.Print(ex.ToString());
    }

    Debug.Print($"finishing on thread {Thread.CurrentThread.ManagedThreadId}");

    return Result.Cancelled;
}

 

0 Likes