0

I'm having difficulty getting a basic OBO example up and running. Any insight is greatly appreciated as I haven't been able to find a complete example that uses my scenario:

A client app (WebApp1) that calls api1, which in turn calls api2 via OBO using the Microsoft.Identity.Web wrapper, running on 3 different localhost ports. In the portal, api1 exposes FunctionA and api2 exposes FunctionB, and WebApp1 has permission+granted consent to api1 and api1 has permission+granted consent to api2. Webapp1 & API1 as clients have secrets defined.

It fails with 401 unauthorized - invalid audience:

AuthenticationHeaderValue.Parameter "error=""invalid_token"", error_description=""The audience 'api2' is invalid"""

My setup:

*appsettings.json - webapp1

       {
      "AzureAd": {
        "Instance": "https://login.microsoftonline.com/",
        "Domain": "mydomain.onmicrosoft.com",
        "TenantId": "123456",
        "ClientId": "webapp1",
"ClientSecret": "webapp1 secret"
        "CallbackPath": "/signin-oidc"
      },
      "DownstreamApi": {
        "BaseUrl": "api://api1",
        "Scopes": "FunctionA"
      }
*appsettings.json - api1 

      "AzureAd": {
    Instance, domain, tenantid = same...
    "ClientId": "api1",
    "Scopes": "FunctionA",
    "ClientSecret": "api1 secret"
  },
  "api2": {
    "BaseUrl": "api2 client id (without API:// prefix",
    "Scopes": "FunctionB"

  },
}

*api2
{
  "AzureAd": {
    "Instance, domain, tenantid = same
    "ClientId": "api2",
    "Scopes": "FunctionB"
  },

webapp1 program.cs snippet:

builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"))
            .EnableTokenAcquisitionToCallDownstreamApi() //initialScopes
            .AddInMemoryTokenCaches();

api1 program.cs snippet:

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"))
    .EnableTokenAcquisitionToCallDownstreamApi() 
    .AddDownstreamApi("api2", builder.Configuration.GetSection("api2"))
    .AddInMemoryTokenCaches();

api1 controller (option 1 results in unauthenticated call with null scopes, option 2 results in 401 unauthorized - invalid audience):

[HttpGet(Name = "GetWeatherForecast")]
public async Task<IActionResult> Get()
{
    // option 1 - 
_scopes =
                configuration["api2:Scopes"].Split(' ').Select(x => $"{_instanceURL}/{x}");
    var testresponse = await _downstreamApi.CallApiForUserAsync(
        "api2",
        options =>
        {
            options.BaseUrl = "https://localhost:7075/";
            options.RelativePath = "weatherforecast";
            options.Scopes = _scopes;
            options.HttpMethod = HttpMethod.Get;
        }
        ); **// 401 - invalid audience**

    // option 2
    var accessToken1 = await _tokenAcquisition.GetAccessTokenForUserAsync(_scopes);

    HttpClient httpClient = new HttpClient();
    httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken1);
    httpClient.BaseAddress = new Uri("https://localhost:7075/");
    var request = new HttpRequestMessage(HttpMethod.Get,
        "weatherforecast");
    var response = await httpClient.SendAsync(request); **// 401 - invalid audience**
    
    if (response.StatusCode != HttpStatusCode.OK)...
Coden00b
  • 87
  • 1
  • 8

1 Answers1

0

There were 3 problems:

  1. I wasn't including the fully qualified scope. I had it as FunctionA instead of api://123456-ff25-47bf-acb6-789/FunctionA.
  2. The BaseUrl needed to be the API's endpoint ("BaseUrl": "https://localhost:7075/weatherforecast").
  3. The deprecated DownstreamWebApi used string for scopes, whereas the replacement DownstreamApi uses an array of strings (IEnumerable), so I had to adjust the json file accordingly.

The simplified end result:

api1 appsettings.json

{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "Domain": "mydomain",
    "TenantId": "mytenant",
    "ClientId": "123456-ff25-47bf-acb6-789",
    "Scopes": [
"api://123456-ff25-47bf-acb6-789/FunctionA"
],
    "ClientSecret": "SUPERSECRET" //from api1
  },
  "api2": {
    "BaseUrl": "https://localhost:7075/weatherforecast",
    "Scopes": [
      "api://123456-ff25-47bf-acb6-890/FunctionB"
    ]
  }
}

api1 controller

public WeatherForecastController(IDownstreamApi downstreamApi)
{
_downstreamApi = downstreamApi;
}
        [HttpGet(Name = "GetWeatherForecast")]
        public async Task<IActionResult> Get()
{
var testresponse = await _downstreamApi.CallApiForUserAsync("api2");
return Ok(testresponse);
}
Coden00b
  • 87
  • 1
  • 8