4

We have implemented authentication on our multi tenant SaaS application through Azure AD (which implements OAuth 2.0). The API's are accessed through an Angular SPA and can also be accessed by other clients (such as registered REST clients, with a dedicated client ID). In case it's relevant: we use the authorization code flow.

On the same application we have an OData API through which we want to provide data access to our users' applications such as Excel. Since we are a multi tenant application this connection needs to be authenticated just as when accessing the "normal" Web API's, such that our data layer can filter for data owned by that tenant.

Even though we only use Microsoft services (through Azure) it doesn't seem evident how Microsoft Excel can connect to the OData feed with the correct authentication method. I have found one article that explains using a Power Query editor with a custom connection definition. I would not consider this approach as it's not a robust solution for typical end users. In addition to this custom configuration approach, I have also read about commercial third party libraries that take over the connection. However for my SaaS customers I can't propose this as a general solution.
I have also found another article that uses an Azure function as a proxy API to get the data. This seems like a robust solution for end users, however I am not sure how this can be done securely and correctly authenticate the API for the correct user (the example in the article is dedicated to 1 tenant).

Q: Is there a robust (out-of-the-box) configuration for end users to access OAuth authenticated OData feed/API's from Excel? If not, what are some secure alternatives I should consider?

Superman.Lopez
  • 1,332
  • 2
  • 11
  • 38
  • Hello@Superman, Could you please let us know the version of Excel Application? Or Please try with excel -add ins to connect Azure AD or explore Excel Rest. Reference:- https://learn.microsoft.com/en-us/graph/api/resources/excel?view=graph-rest-1.0 https://learn.microsoft.com/en-us/sharepoint/dev/general-development/excel-services-rest-api – Nishant Nov 05 '20 at 08:59
  • @Nishant thanks for your response: for some reason I never received a notification on stackoverflow, hence my late response. Both links seem to focus on the Web app accessing the Excel workbook. This is not of our interest as the Excel workbooks are owned by our clients, and are numerous and ad hoc. Instead we want our clients to have access to app data via our OData feed. But this needs to be secure, and we need to identify the tenant correctly. Hence we want to use OAuth on top of OData API, accessible from Excel. Our clients typically use Microsoft/Office 365 in Win environments. – Superman.Lopez Jun 19 '21 at 14:59

2 Answers2

5

You can do OAuth 2.0 authentication on AzureAD from your client application such as Excel, Word, Powerpoint and Publisher. This works for opening entire files (Office 2016 or above) or import data from a webpage (Office 2019 / M365-apps).

The way you set up OAuth authentication will differ a bit from the regular and well documenten OAuth flow as you use it in your REST clients.

Instead of sending a header that forwards the client to https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize you need to return the http response code 401 (unauthorized) and add the next headers:

WWW-Authenticate: Bearer resource="https://management.azure.com/" client_id="{the client id of your registered app in Azure AD}", trusted_issuers="00000001-0000-0000-c000-000000000000@*", token_types="app_asserted_user_v1 service_asserted_app_v1", authorization_uri="https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize",Basic Realm=""'

In Azure AD in your app registration:

  • In API permissions:
    • Azure Active Directory Graph > User.Read In
  • Expose an API:
    • Add a scope: https://{fqdn of your api}/email (email or other attribute you use for authentication)
    • Authorized client application > Add a client application: d3590ed6-52b3-4102-aeff-aad2292ab01c (the ID of MS Office)

Note: all this is not documented at all. I found out about the headers by sniffing the traffic between Outlook and O365.

Btw: If you serve files using the webdav protocol: opening a file in Excel from a webpage works well with javascript:

location.href = "ms-excel:ofe|https://{yourAPI}/your_output.xlsx"
ARN
  • 679
  • 7
  • 15
  • Thanks so much, this looks very promising. I was hoping to somehow find the client application ID of Excel, and now know it's the ID of MS Office! – Superman.Lopez Jul 04 '21 at 02:36
  • 1
    For Excel, Word,.. it's the same ID. The version of office does not matter. – ARN Jul 05 '21 at 10:37
  • This is close to black magic to me ;) but it worked! I am running into some other issues about the validity of the resource, but the authentication itself seemed to have worked. Thanks so much! – Superman.Lopez Nov 05 '21 at 02:48
  • @ARN, Would you mind expanding on this answer a bit? In particular, how do you change the header to return the 401 code? Is that done in the controller / app or AzureAD? – gwruck Aug 03 '22 at 21:49
  • @Supeman.Lopez, did you end up getting this working fully? I'm guessing that the resource validity issue that you mention relates to the error: 'invalid_resource: AADSTS500011:The resource principal named ........ was not found in the tenant named ..... This can happen if the application has not been installed by the administrator '. I'd be very keen to know how you got around that error. – gwruck Aug 05 '22 at 01:45
  • 1
    Yes, I did get it working fully. I don't recall what was the issue with the validity of the resource. I am setting up a second environment with this soon. If I recall what the solution was with the resource issue I will let you know. – Superman.Lopez Aug 07 '22 at 14:14
  • @gwruck The issue with the validity issue was solved by changing the api scope to the http domain name. This is included in the answer by ARN, but I had missed this the very first time I tried to implement this. So instead of "api://xxxxguidxxx" make sure the scope is "https:// example.com" – Superman.Lopez Oct 26 '22 at 18:13
4

ANSWER EDITED TO FINAL WORKING VERSION

Thanks in part to comments by others on this thread, I have been able to get this working. Here is my final solution (edited from the original answer that had some of the pieces of the puzzle missing)

I registered the app in Azure, exposed the API, changed the Application ID URI to my verified app url and added the user_impersonation scope . enter image description here

You will need to ensure that you have a verified Custom Domain Name in Azure AD. enter image description here

I accepted the default API permissions. enter image description here

I created a .net 6 WebApi project, made a simple odata controller.

[Route("odata")]
public class WBSController : BaseODataController
{
    public WBSController(IDbContextFactory<TableDbContext> db) : base(db)
    {

    }

    [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
    [EnableQuery]
    [HttpGet("WBS")]
    public IQueryable<WBS> Get()
    {
        IQueryable<WBS> ret = Db.WBS;

        return ret;
    }
}

I then configured the API in Program.cs . (NOTE: for client Id - use your App Id Uri.

Program.cs

.AddMicrosoftIdentityWebApi(
     opt => //JwtBearerOptions
    {
        opt.Audience = "[Your App Id Uri eg. acme.com.au]";
        opt.Events = new JwtBearerEvents()
        {
            OnChallenge = async ctxv =>  
            {
                opt.Challenge =
                    @"Bearer  realm="""", client_id=""[Your App Id Uri]"", trusted_issuers=""00000001-0000-0000-c000-000000000000@*"", token_types=""app_asserted_user_v1 service_asserted_app_v1"",  authorization_uri=""https://login.microsoftonline.com/[Your Tenant Id]/oauth2/v2.0/authorize""";  //This is the magic that forces the Microsoft login dialog to open
            }
        };
    },
    opt =>   //MicrosoftIdentityOptions
    {
        opt.Instance = "https://login.microsoftonline.com/";
        opt.ClientId = "[Your App Id Uri]";
        opt.TenantId = "common";
    },
    $"{JwtBearerDefaults.AuthenticationScheme}"  //Scheme Name
);

I thought this was a big breakthrough as I was able to pick an account and sign-in. enter image description here

If you come across the following error, it means that you have not set the App ID in Azure AD to your verified application url: invalid_resource:AADSTS500011: The resource principal ...... was not found in the tenant named ..... enter image description here

If you get any errors at all, or it doesn't do anything, run a trace using Fiddler. That is how I determined it was looking for the user_impersonation scope.

If you get all of these steps right, you should end up with a refreshable table of your OData data in Excel.

I note that MS says it is not supported to connect to "arbitrary" services like this. https://learn.microsoft.com/en-us/power-query/connectors/odatafeed#authenticating-to-arbitrary-services.

However I believe that this is a very good way of providing Excel super users the ability to access structured data from a Web Api. I hope this post saves you the many hours it took me to get this going.

gwruck
  • 341
  • 2
  • 9
  • 1
    `invalid_resource:AADSTS500011` can be fixed by renaming Application ID URI to https://localhost:7086 in the Expose an API screen. – MotKohn Aug 23 '22 at 18:57
  • @MotKohn, That was another very big piece of the puzzle - thankyou. It turns out that you can't set the AppId to local host per this error [Failed to update Application ID URI application property. Error detail: Values of IdentifierUris property must use a verified domain of the organization or its subdomain] . If I changed the appId to a url that I had already verified within my AzureAD tennant, I could then complete the signin step in the Excel Datafeed form. ...that then brought me to a couple more problems before I could get this to work. – gwruck Aug 25 '22 at 01:38
  • 1
    @MotKohn, the final 2 pieces of the puzzle that I needed to solve before I got this work work were that I needed to expose a scope called [user_impersonation] in Azure/Expose an Api. I also had to set the ClientId to my AppId (ie. the fully qualified external domain of my app). After doing all that, I can now get a refreshable Odata feed in Excel. I'm going to edit my answer above with the final resolution of the problem so that I never have to work out how to do this again from scratch. – gwruck Aug 25 '22 at 01:44
  • 2
    @Mat Kohn I guess the only question that remains now in my mind is why is this not a standard way of working with OData in Excel. I have been trying to find a solution to this problem on and off for a number of months now and the solution needed many levels of workarounds and a great amount of perseverence. I believe that this approach is much better than having to create a custom PowerQuery Connector (whenever they finally deploy those to Excel). However, does it introduce any security vulnerabilities beyond the well know Odata limitations? – gwruck Aug 25 '22 at 01:50
  • Yes, that took a lot of persistence on my part too, and I do feel the same that we are hacking something that should be straightforward. I also thank you for being able to build upon your work. Regarding security I'm no expert, but once the sign on process is done the data exchanges look pretty standard oauth where a token is passed and validated on each call. BTW I was able to get localhost to work I mean https:\\localhost: , but I think at this point if it works there is no reason to bother. I'm just writing this for others thgat may want to try this. Thanks again for this post – MotKohn Aug 25 '22 at 13:56