Azure,  Microsoft Dataverse,  Power Apps

Implementing OAuth 2.0 On-Behalf-Of Flow for Dataverse Custom Connector

In most cases, the functionality provided by the pre-built connectors is fully sufficient to meet the given requirements. However, there are times when custom connectors are needed to implement the missing functionality, to centralise the shared business logic and to optimise the way operations on the connectors are called and consumed in order to reduce the complexities they impose on the client, etc. When implementing a custom connector, the operations must often be restricted to the caller’s permission. In order to do this, it must persist the caller’s identity when requesting the underlying connector’s API and, in turn, the downstream APIs it accesses. We will discuss how to do this by implementing a custom connector using the OAuth 2.0 on-behalf-of flow.

Here is what we are going to cover in this post:

  • What is OAuth 2.0 on-behalf-of flow?
  • Set up Azure App Registrations (Service and Client)
  • Set up a web API project using Visual Studio
  • Set up code configuration
  • Set up Swagger using Swashbuckle
  • Call downstream APIs
  • Configure the custom connector

 

What is OAuth 2.0 On-Behalf-Of Flow?

The on-behalf-of (OBO) authentication flow is specifically used in the scenario where an application calls a web API which, in turn, calls another web API. In this flow, the objective is to propagate the delegated user identity and permissions throughout the entire request chain. To do this, the web API which is calling the downstream web API must acquire an access token on behalf of the user from the Microsoft Identity platform to gain access to the resources secured by the downstream web API.

In this post, we will implement a scenario where the custom API for our custom connector needs to access resources on the Microsoft Graph and Microsoft Dataverse. To understand how this OBO flow works for this scenario, I’ve broken down the flow steps using a canvas app as the consuming client for our custom connector:

  1. When the user opens the canvas app for the first time, depending on the connectors used on the app, there will be an initial prompt to establish connections for the connectors used, our custom connector being one of them
  2. Through the established connection, an access token is acquired to call the underlying custom API for the custom connector
  3. The custom API, in turn, uses the access token to request another access token on behalf of the user to call Microsoft Graph
  4. The acquired token is used to call the Microsoft Graph API
  5. The custom API repeats steps 3-4, but to call the Microsoft Dataverse API instead
  6. The custom API can then apply some business logic and transformation to the data before returning it through the custom connector to the canvas app

You can learn more about this authentication flow in the official documentation. Now let’s go ahead and implement this!

 

Set up Azure App Registrations (Service and Client)

Setting up the Azure app registration correctly is really important to get the OBO flow working with our implementation. If you are unfamiliar with creating an Azure app registration then you can learn how to do so by using this link, but make sure to apply the configuration as stated in this post!

We need to create two app registrations, one for the custom API and another one for the client (i.e. custom connector) that calls the custom API. To describe their logical functions, I’ve named the app registration that secures the custom API as Service (OBO-Connector-Core-API-Service), and the calling app as Client (OBO-Connector-Core-API-Client).

Set up OBO-Connector-Core-API-Service

  1. Navigate to the App Registration section of the Azure Portal (Use this link to navigate directly). Select + New Registration
  2. On the Register an Application page, enter the following information:
    • Name: OBO-Connector-Core-API-Service
    • Supported account types: Accounts in this organizational directory only
    • Redirect URI: <leave empty>
  3. On the Overview page, copy and note down the Application (client) ID
  4. On the Certificates & secrets page, + New client secret. Create a secret, copy and note down the secret Value
  5. On the API permissions page, add these delegated permissions by selecting + Add a permission:
  6. After selecting all the permissions, make sure to ✔ grant admin consent. Note: if admin consent cannot be given, you must modify the manifest of this app registration to include the client id of the OBO-Connector-Core-API-Client in the knownClientApplications attribute. Learn more about it here and here.
  7. On the Expose an API page, enter the following information:
    • Set the Application ID URI. This should auto-generate a URI in the format api://<guid>
    • Add two custom scopes to restrict access to the resources and operations secured by the custom API

Set up OBO-Connector-Core-API-Client

  1. Navigate to the App Registration section of the Azure Portal (Use this link to navigate directly). Select + New Registration
  2. On the Register an Application page, enter the following information:
    1. Name: OBO-Connector-Core-API-Client
    2. Supported account types: Accounts in this organizational directory only
    3. Redirect URI: <leave empty but we will come back to this later>
  3. On the Overview page, copy and note down the Application (client) ID
  4. On the Certificates & secrets page, + New client secret. Create a secret, copy and note down the secret Value
  5. On the API permissions page, add these delegated permissions by selecting + Add a permission:

    Note: OBO-Connector-Core-API-Service can be found under My APIs. This will only show up if you have set an application ID URI on the OBO-Connector-Core-API-Service app registration.

With these two app registrations configured, let’s set up the project.

 

Set up a Web API Project Using Visual Studio

Create a VS project using Empty template for ASP.NET Core Web Application and choose ASP.NET Core 3.1 as the framework. Make sure to enable Configure for HTTPS:

Install the following NuGet packages:

<PackageReference Include="Microsoft.Identity.Web" Version="1.9.1" />
<PackageReference Include="Microsoft.Identity.Web.MicrosoftGraph" Version="1.9.1" />
<PackageReference Include="Microsoft.OpenApi" Version="1.2.3" />
<PackageReference Include="Microsoft.PowerPlatform.Dataverse.Client" Version="0.4.12" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.1.4" />
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="6.1.4" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.1.4" />

To demonstrate the OBO flow I included these files under my project:

 

Set up Code Configuration

In appsettings.json, insert these configurations:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "ClientId": "<Client ID copied from OBO-Connector-Core-API-Service app registration>",
    "ClientSecret": "<Client Secret copied from OBO-Connector-Core-API-Service app registration>",
    "Audience": "<Client ID copied from OBO-Connector-Core-API-Service app registration>",
    "Domain": "<Tenant Name>.onmicrosoft.com",
    "TenantId": "<Tenant ID>"
  },
  "Client": {
    "ClientId": "<Client ID copied from OBO-Connector-Core-API-Client app registration>",
    "ClientSecret": "<Client Secret copied from OBO-Connector-Core-API-Client app registration>"
  },
  "GraphApi": {
    "BaseUrl": "https://graph.microsoft.com/v1.0",
    "Scopes": "user.read directory.read.all"
  },
  "DataverseApi": {
    "BaseUrl": "https://<Power Apps Environment Name>.crm6.dynamics.com",
    "Scopes": "user_impersonation"
  },
  "DownstreamDataverseApi": {
    "BaseUrl": "https://<Power Apps Environment Name>",
    "Scopes": "https://<Power Apps Environment Name>/user_impersonation"
  },
  "AllowedHosts": "*"
}
Note: From the official documentation, the "Audience" does not need to be set if the proposed Application ID URI by the app registration portal is accepted. I couldn't get it to recognize the audience hence explicitly defined the value here. 

In Startup.cs:

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Identity.Web;

public void ConfigureServices(IServiceCollection services)
{
    ...

    services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
            .AddMicrosoftIdentityWebApi(Configuration, "AzureAd")
            .EnableTokenAcquisitionToCallDownstreamApi()
            .AddMicrosoftGraph(Configuration.GetSection("GraphApi"))
            .AddDownstreamWebApi(
                "DownstreamDataverseApi",
                Configuration.GetSection("DownstreamDataverseApi")
            ).AddInMemoryTokenCaches();
    
    ...
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    ...

    app.UseAuthentication();
    app.UseAuthorization();

    ...
}

The most interesting section to discuss on the Startup.cs is the usage of Microsoft.Identity.Web NuGet package to implement the authentication middleware. The line .AddMicrosoftIdentityWebApi(Configuration, "AzureAd") secures the API with the Microsoft Identity platform by using the configuration set in the AzureAd section of the appsettings.json. These are details obtained from the OBO-Connector-Core-API-Service app registration.

In order to acquire a token for the downstream API, the EnableTokenAcquisitionToCallDownstreamApi method is added. This exposes ITokenAcquisition service which can be injected directly on the controller constructor using the dependency injection (DI). This service exposes methods that can be used to explicitly request a token for the user.

The token acquisition can be streamlined even further for the downstream APIs by using the following extension methods:

  • AddMicrosoftGraph(Configuration.GetSection("GraphApi")) – directly exposes the GraphServiceClient on the controller constructor using DI
  • AddDownstreamWebApi("DownstreamDataverseApi", Configuration.GetSection("DownstreamDataverseApi") – directly exposes the IDownstreamWebApi service on the controller constructor using DI. This adds a named downstream web API service related to a specific configuration section in appsettings.json

The great thing about these two methods is that they abstract away the token acquisition process as well as authorizing the requests using the bearer token.

 

Set up Swagger using Swashbuckle

I’ve configured the Swashbuckle Swagger generator and enabled its middleware to generate the JSON Open API definition and the Swagger UI in Startup.cs as follows:

using Microsoft.OpenApi.Models;
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;

public void ConfigureServices(IServiceCollection services)
{
    ...

    services.AddSwaggerGen(setupAction =>
    {
        setupAction.SwaggerDoc("v1", new OpenApiInfo
        {
            Title = "Obo Connector Core Api",
            Version = "v1",
            Description = "Obo Connector Core Api",
            Contact = new OpenApiContact
            {
                Name = "Tae Rim Han",
                Email = "taerim.han@test.com",
                Url = new Uri("https://taerimhan.com")
            }
        });

        setupAction.EnableAnnotations();

        setupAction.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme
        {
            Type = SecuritySchemeType.OAuth2,
            Flows = new OpenApiOAuthFlows
            {
                Implicit = new OpenApiOAuthFlow
                {
                    AuthorizationUrl
                        = new Uri($"https://login.windows.net/{Configuration["AzureAd:TenantId"]}/oauth2/authorize", UriKind.Absolute),
                    Scopes = new Dictionary<string, string>
                    {
                        { "user_impersonation", "Access Dataverse data" },
                        { "access_as_user", "Access MS Graph data" }
                    }
                }
            }
        });

        setupAction.AddSecurityRequirement(new OpenApiSecurityRequirement
        {
            {
                new OpenApiSecurityScheme()
                {
                    Reference = new OpenApiReference
                    {
                        Type = ReferenceType.SecurityScheme,
                        Id = "oauth2"
                    }
                },
                new List<string>()
            }
        });

        var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
        var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
        setupAction.IncludeXmlComments(xmlPath);
    });
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        ...

        app.UseSwagger(setupAction =>
        {
            setupAction.SerializeAsV2 = true;
            setupAction.PreSerializeFilters.Add((swagger, httpReq) =>
            {
                swagger.Servers = new List<OpenApiServer>
            {
                new OpenApiServer { Url = $"{httpReq.Scheme}://{httpReq.Host.Value}" }
            };
            });
        });
        app.UseSwaggerUI(setupAction =>
        {
            setupAction.OAuthClientId(Configuration["Client:ClientId"]);
            setupAction.OAuthClientSecret(Configuration["Client:ClientSecret"]);
            setupAction.OAuthRealm(Configuration["AzureAd:ClientId"]);
            setupAction.OAuthAppName("Obo Connector Core Api");
            setupAction.OAuthScopeSeparator(" ");
            setupAction.OAuthAdditionalQueryStringParams(new Dictionary<string, string>
            {
                { "resource", Configuration["AzureAd:ClientId"] }
            });
            setupAction.SwaggerEndpoint("/swagger/v1/swagger.json", "Obo Connector Core Api");
            setupAction.RoutePrefix = string.Empty;
        });
    }

    ...
}

There is another post I wrote which goes into more detail on what each Swagger configuration means and how to use the generated Open API definition to create the custom connector. Here, I’m going to highlight what I’ve done differently to make the most out of Swagger Gen / UI to test the API when debugging locally.

In Visual Studio, open the project properties, under Debug, make sure the Enable SSL is checked and copy and note down the URL for the application on local IIS Express:

Back to Azure Portal, to the OBO-Connector-Core-API-Client app registration, under Authentication section, + Add a platform, choose Web as the platform and paste the localhost URL as the redirect URI and turn on the implicit grant settings:

Enabling implicit flow is handy when using Swagger UI in the browser, as you don’t need to deal with the CORS error on preflight when authenticating to get the token. I added .AddSecurityDefinition with security scheme type of OAuth2 and explicitly specified the flow to be implicit grant type. The scopes that we defined in the OBO-Connector-Core-API-Service have been added to this security scheme, these scopes will be used later to filter the access to the API operations.

With the above setup, Swagger UI will expose the Authorize button with these settings:

We can prepopulate some of the values to use for the authorization request using .UseSwaggerUI(setupAction => { ... }).

Note: To make this post more readable, I added a very simple .AddSecurityRequirement over the API operations to visually indicate that each operation requires authorization (i.e. lock icon). For Open API document generation purposes, we should implement IOperationFilter to figure out which operations require authorization and add appropriate 401 and 403 responses. You can find an example of this here.

 

Call Downstream APIs!

I’m going to cover three ways to call the Microsoft Dataverse API adhering to the OBO flow – 1) acquiring an access token on behalf of the user and 2) use it to call the API.

Before we do that, let’s not forget to create a Power Apps application user using the OBO-Connector-Core-API-Service client id. Here is a link if you need help setting this user up. Unlike using the CallerId to impersonate the user when calling the Dataverse API, this application user does not require the prvActOnBehalfOfAnotherUser privilege to delegate or any security roles assigned as it relies purely on the user security and permissions.

Using HttpClient

We can use HttpClient to demonstrate the most basic way to call REST APIs. The GetAccessTokenForUserAsync method on the ITokenAcquisition service is used to acquire the access token on behalf of the user and for this method we will pass user_impersonation as the scope for the Microsoft Dataverse API. Once the token is acquired, we can include it in the Authorization request header.

Note: [RequiredScope(...)] annotation on the controller defines the custom scopes that are required when calling the operations on the custom API. Not the same as the scopes required for the downstream APIs. 
[Authorize]
[ApiController]
[Produces("application/json")]
[Route("api/[controller]")]
[RequiredScope("user_impersonation")]
public class HttpClientController : Controller
{
    private readonly string _instanceUrl;
    private readonly IEnumerable<string> _scopesToAccessDataverseApi;
    private readonly ITokenAcquisition _tokenAcquisition;

    public HttpClientController(ITokenAcquisition tokenAcquisition, IConfiguration configuration)
    {
        _instanceUrl = configuration["DataverseApi:BaseUrl"];
        _scopesToAccessDataverseApi 
            = configuration["DataverseApi:Scopes"].Split(' ').Select(x => $"{_instanceUrl}/{x}");
        _tokenAcquisition = tokenAcquisition;
    }

    [HttpGet("accounts")]
    [ProducesDefaultResponseType()]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    [ProducesResponseType(StatusCodes.Status200OK)]
    [SwaggerOperation(
        Summary = "Get accounts (HttpClient)",
        Description = "Get list of accounts using HttpClient",
        OperationId = "GetAccountsHttpClient"
    )]
    public async Task<IEnumerable<Account>> Get()
    {
        var accessToken 
            = await _tokenAcquisition.GetAccessTokenForUserAsync(_scopesToAccessDataverseApi);
        var accounts = new List<Account>();

        using (var httpClient = new HttpClient())
        {
            httpClient.BaseAddress = new Uri(_instanceUrl);
            httpClient.Timeout = new TimeSpan(0, 2, 0);
            httpClient.DefaultRequestHeaders.Add("OData-MaxVersion", "4.0");
            httpClient.DefaultRequestHeaders.Add("OData-Version", "4.0");
            httpClient.DefaultRequestHeaders.Accept.Add(
                new MediaTypeWithQualityHeaderValue("application/json")
            );
            httpClient.DefaultRequestHeaders.Authorization 
                = new AuthenticationHeaderValue("Bearer", accessToken);
            var response 
                = await httpClient.GetAsync($"{_instanceUrl}/api/data/v9.1/accounts?$select=name,accountid");
            if (response.StatusCode == System.Net.HttpStatusCode.OK)
            {
                var content = await response.Content.ReadAsStringAsync();
                dynamic responseObject = JsonConvert.DeserializeObject(content);
                foreach(var value in responseObject.value)
                {
                    accounts.Add(new Account { Id = value.accountid, Name = value.name });
                }
            }
        }
        return accounts;
    }
    ...
}

Using ServiceClient (Microsoft.PowerPlatform.Dataverse.Client)

It is hard not to dream about using Microsoft.PowerPlatform.Dataverse.Client NuGet package when you are working on a .NET Core application and you want to access the SDK functionality as you might find in Microsoft.Xrm.Tooling.Connector.CrmServiceClient and underlying Microsoft.Xrm.Sdk.Client libraries from your application. This package is currently versioned at 0.4.12 (at the time of writing) and it’s in alpha state so not advisable to use it for production environments. That doesn’t stop us from having fun with it! In our example, we initialise the ServiceClient by providing the Power Apps instance URL and token provider function. This token provider function simply wraps the call to the GetAccessTokenForUserAsync method.

[Authorize]
[ApiController]
[Produces("application/json")]
[Route("api/[controller]")]
[RequiredScope("user_impersonation")]
public class ServiceClientController : Controller
{
    private readonly string _instanceUrl;
    private readonly IEnumerable<string> _scopesToAccessDataverseApi;
    private readonly ITokenAcquisition _tokenAcquisition;

    public ServiceClientController(ITokenAcquisition tokenAcquisition, IConfiguration configuration)
    {
        _instanceUrl = configuration["DataverseApi:BaseUrl"];
        _scopesToAccessDataverseApi 
            = configuration["DataverseApi:Scopes"].Split(' ').Select(x => $"{_instanceUrl}/{x}");
        _tokenAcquisition = tokenAcquisition;
    }

    [HttpGet("accounts")]
    [ProducesDefaultResponseType()]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    [ProducesResponseType(StatusCodes.Status200OK)]
    [SwaggerOperation(
        Summary = "Get accounts (Microsoft.PowerPlatform.Dataverse.Client)",
        Description = "Get list of accounts using Microsoft.PowerPlatform.Dataverse.Client",
        OperationId = "GetAccountsServiceClient"
    )]
    public async Task<IEnumerable<Account>> Get()
    {
        async Task<string> tokenProvider(string _)
        {
            return await _tokenAcquisition.GetAccessTokenForUserAsync(_scopesToAccessDataverseApi);
        }

        var serviceClient = new ServiceClient(new Uri(_instanceUrl), tokenProvider);
        var response = await serviceClient.RetrieveMultipleAsync(new QueryExpression("account") 
        {
            ColumnSet = new ColumnSet("name", "accountid")
        });

        var accounts = response.Entities.Select(x => new Account { Id = x.Id, Name = x["name"] as string });
        return accounts;
    }
    ...
}

Using IDownstreamWebApi

The extension method .AddDownstreamWebApi defined in Startup.ConfigureServices allows for the injection of the IDownStreamWebApi service into a controller constructor. This calls the downstream web API based on the name of the service described in the configuration. In our example, the service name is “DownstreamDataverseApi” which was specified during the .AddDownstreamWebApi call in the Startup.ConfigureServices and referred to when using the .CallWebApiForUserAsync method. The .CallWebApiForUserAsync method then abstracts away the detail of the token acquisition as well as the request authorization to call the API endpoint. The configuration in the appsettings.json must include the base URL of the downstream web API and the scopes required for calling the downstream API.

Troubleshoot Note: For the configuration of the ITokenAcquisition.GetAccessTokenForUserAsync we only had to specify the names of the scopes (e.g. user.read). However, for the IDownstreamWebApi configuration, I had to provide the fully qualified names of the scopes (i.e. http://<baseurl>/<scope name>) to get the code working.  
[Authorize]
[ApiController]
[Produces("application/json")]
[Route("api/[controller]")]
[RequiredScope("user_impersonation")]
public class DownstreamApiController : Controller
{
    private IDownstreamWebApi _downstreamWebApi;
    private const string ServiceName = "DownstreamDataverseApi";

    public DownstreamApiController(IDownstreamWebApi downstreamWebApi)
    {
        _downstreamWebApi = downstreamWebApi;
    }

    [HttpGet("accounts")]
    [ProducesDefaultResponseType()]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    [ProducesResponseType(StatusCodes.Status200OK)]
    [SwaggerOperation(
        Summary = "Get accounts (IDownstreamWebAPI)",
        Description = "Get list of accounts using IDownstreamWebAPI",
        OperationId = "GetAccountsIDownstreamWebAPI"
    )]
    public async Task<IEnumerable<Account>> Get()
    {
        var accounts = new List<Account>();

        var response = await _downstreamWebApi.CallWebApiForUserAsync(
            ServiceName,
            options =>
            {
                options.RelativePath = $"/api/data/v9.1/accounts?$select=accountid,name";
                options.HttpMethod = HttpMethod.Get;
            });

        if (response.StatusCode == System.Net.HttpStatusCode.OK)
        {
            var content = await response.Content.ReadAsStringAsync();
            dynamic responseObject = JsonConvert.DeserializeObject(content);
            foreach (var value in responseObject.value)
            {
                accounts.Add(new Account { Id = value.accountid, Name = value.name });
            }
        }
        return accounts;
    }
    ...
}

Using GraphServiceClient

The extension method .AddMicrosoftGraph defined in Startup.ConfigureServices allows for the injection of the GraphServiceClient into a controller constructor. The required scopes for the Microsoft Graph API are specified using the configuration section in the appsettings.json. Like the IDownStreamWebApi service, the Microsoft.Identity.Web handles the token acquisition and request authorization to call the Microsoft Graph API.

[Authorize]
[ApiController]
[Produces("application/json")]
[Route("api/[controller]")]
[RequiredScope(new string[] { "access_as_user", "user_impersonation" })]
public class UserController : Controller
{
    private IDownstreamWebApi _downstreamWebApi;
    private const string ServiceName = "DownstreamDataverseApi";
    private readonly GraphServiceClient _graphServiceClient;

    public UserController(GraphServiceClient graphServiceClient, IDownstreamWebApi downstreamWebApi)
    {
        _graphServiceClient = graphServiceClient;
        _downstreamWebApi = downstreamWebApi;
    }
    
    [HttpGet("users")]
    [ProducesDefaultResponseType()]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    [ProducesResponseType(StatusCodes.Status200OK)]
    [SwaggerOperation(
        Summary = "Get users",
        Description = "Get list of licensed users",
        OperationId = "GetUsers"
    )]
    public async Task<IEnumerable<Entities.User>> GetUsers()
    {
        var users = new List<Entities.User>();

        var response = await _downstreamWebApi.CallWebApiForUserAsync(
            ServiceName,
            options =>
            {
                options.RelativePath = $"/api/data/v9.1/systemusers?$select=systemuserid,fullname,domainname,title,azureactivedirectoryobjectid&$filter=isdisabled eq false and islicensed eq true";
                options.HttpMethod = HttpMethod.Get;
            });

        if (response.StatusCode == System.Net.HttpStatusCode.OK)
        {
            var content = await response.Content.ReadAsStringAsync();
            dynamic responseObject = JsonConvert.DeserializeObject(content);
            foreach (var value in responseObject.value)
            {
                users.Add(new Entities.User
                {
                    Id = value.systemuserid,
                    FullName = value.fullname,
                    DomainName = value.domainname,
                    Title = value.title,
                    AzureActiveDirectoryObjectId = value.azureactivedirectoryobjectid
                });
            }
        }
        return users;
    }
    
    [HttpGet("users/{userId}/licenses")]
    [ProducesDefaultResponseType()]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    [ProducesResponseType(StatusCodes.Status200OK)]
    [SwaggerOperation(
        Summary = "Get user licenses",
        Description = "Get user licenses",
        OperationId = "GetUserLicenses"
    )]
    public async Task<IEnumerable<Entities.License>> GetUserLicenses(string userId)
    {
        var licenseDetailsCollection = await _graphServiceClient.Users[userId].LicenseDetails.Request().GetAsync();
        var licenses = new List<Entities.License>();
        while (licenseDetailsCollection.Count > 0)
        {
            foreach (var licenseDetails in licenseDetailsCollection)
            {
                licenses.Add(new Entities.License
                {
                    SkuId = licenseDetails.SkuId,
                    SkuPartNumber = licenseDetails.SkuPartNumber
                });
            }

            if (licenseDetailsCollection.NextPageRequest != null)
            {
                licenseDetailsCollection = await licenseDetailsCollection.NextPageRequest.GetAsync();
            }
            else
            {
                break;
            }
        }

        return licenses;
    }
    ...
}

As you can see in this example, the controller utilises both the GraphServiceClient and IDownstreamWebApi services in the same controller. Both downstream APIs are accessed using the delegated permission of the user through the OBO flow. The example scenario shown here is quite simple but you could imagine using this for more complex scenarios.

 

Configure the Custom Connector

Once the web API is implemented, publish it to the Azure App Service. Download a copy of the Open API definition from https://<app service url>/swagger/v1/swagger.json and use it to create the custom connector from make.powerapps.com > Data > Custom Connectors. Instructions on how to set up the custom connector using the Open API definition can be found here. For setting up the Security section, populate it with the following information and ✔ Create Connector to save and generate the Redirect URL:

Copy and note down the value in the Redirect URL and add it to OBO-Connector-Core-API-Client app registration > Authentication > Web platform > Redirect URIs:

As a final step, test the custom connector by creating a new connection.

To use the custom connector, create a new Canvas App and add the custom connector from the data source section:

Request the API and bind the result to a control as follows:

Now when you share the Canvas App with another user, the result will be trimmed to what that user has permission to access!

Hope you enjoyed the post, let me know your thoughts in the comments below or any of my contact channels 😎