5

I am having difficulty in understanding how to reuse a user's security token, to authenticate them as their data request flows through multiple Web APIs.

  • Console App - C# / Net Framework 4.7.x console application.
  • WebAPI 1 - C# / .Net Core 2.2 MVC WebAPI application.
  • WebAPI 2 - C# / .Net Core 2.2 MVC WebAPI application.

Currently, these are all configured as standalone applications in their own Visual Studio 2019 Solutions running on my dev box, but (once working!!) will each be hosted in Azure as separate entities in their own right.

Essentially, the user authenticates within the Console App, validating their credentials from Azure Active Directory. Following this sample on GitHub, I've got my Console App to call off to WebAPI 1 successfully, and returns data.

However, I want WebAPI 1 to call WebAPI 2 during the call and to retrieve other data as part of the dataset for the Console App, and this is the part I'm stuck with.

WebAPI 2 is configured in the Azure Portal exactly the same as WebAPI 1, with the exception of different Application Client Ids, etc.

As part of the sample (mentioned above), I am able to have WebAPI 1 call off to Microsoft's Graph API, before returning data back to the calling Console App, so I don't think I'm a way off of this. Here's the code to call the Graph API:

    public async Task<string> CallGraphApiOnBehalfOfUser()
    {
        string[] scopes = { "user.read" };

        // we use MSAL.NET to get a token to call the API On Behalf Of the current user
        try
        {
            string accessToken = await _tokenAcquisition.GetAccessTokenOnBehalfOfUser(HttpContext, scopes);
            dynamic me = await CallGraphApiOnBehalfOfUser(accessToken);
            return me.userPrincipalName;
        }
        catch (MsalUiRequiredException ex)
        {
            _tokenAcquisition.ReplyForbiddenWithWwwAuthenticateHeader(HttpContext, scopes, ex);
            return string.Empty;
        }
    } 

    private static async Task<dynamic> CallGraphApiOnBehalfOfUserOriginal(string accessToken)
    {
        //
        // Call the Graph API and retrieve the user's profile.
        //
        HttpClient client = new HttpClient();
        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
        HttpResponseMessage response = await client.GetAsync("https://graph.microsoft.com/v1.0/me");
        string content = await response.Content.ReadAsStringAsync();
        if (response.StatusCode == HttpStatusCode.OK)
        {
            dynamic me = JsonConvert.DeserializeObject(content);
            return me;
        }

        throw new Exception(content);
    }

My plan was to change the URL in the above code to point to the address of WebAPI 2, but it fails during authentication. IF I remove the [Authorize] class attribute on my Controller within WebAPI 2, it does successfully make a connection and return the expected data, but with the attribute on, it doesn't even hit a breakpoint on the Controller, suggesting to me that the problem is with the bearer token that I'm trying to use OR that WebAPI 2 is not configured properly.

Getting a copy of the security token and trying to re-use this mid-flight also doesn't work, as I assume that the token is for the WebAPI 1 and is therefore invalid for use with WebAPI 2.

Should I be doing pass-along authentication like this? (It feels dirty to hard-code user credentials into WebAPI 1 which are able to access WebAPI 2, so I don't want to do that. Plus, if the user credentials need changing, I've got a redeployment just for that.)

Is there a better way to do what I'm trying to do?

If you need me to provide more information to explain anything I've done, I certainly can do.

UPDATE 1: Here's the Startup.cs:

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddProtectWebApiWithMicrosoftIdentityPlatformV2(Configuration)
                .AddProtectedApiCallsWebApis(Configuration, new string[] { "user.read", "offline_access" })
                .AddInMemoryTokenCaches();

        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
    }

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

        app.UseHttpsRedirection();
        app.UseAuthentication();
        app.UseMvc();
    }

UPDATE 2: Similar Stack Overflow post I've since found this SO post, which @philippe-signoret describes in his answer and is exactly what I'm after.

UPDATE 3: unauthorized response when calling WebAPI 2

Here's the error message i get back from the call:

{StatusCode: 401, ReasonPhrase: 'Unauthorized', Version: 1.1, Content: System.Net.Http.HttpConnection+HttpConnectionResponseContent, Headers:
{
   Server: Kestrel
   WWW-Authenticate: Bearer error="invalid_token", error_description="The signature is invalid"
   X-SourceFiles: =?UTF-8?B?*<random-looking-code>*
   X-Powered-By: ASP.NET
   Date: Fri, 31 May 2019 09:48:31 GMT
   Content-Length: 0
}}

As I mentioned earlier, if I remove the [Authorize] attribute from my Controller's class, the call goes through as expected.

Brett Rigby
  • 6,101
  • 10
  • 46
  • 76
  • what is the implementation for ConfigureService? especially AddAuthorization – ChizT May 30 '19 at 15:14
  • Very recent sample that touches on the details of this issue: https://azure.microsoft.com/en-us/resources/samples/active-directory-dotnet-webapi-onbehalfof/ – David Tansey May 30 '19 at 15:16
  • @ranton187 - have added some extra code to the question to show this. Thanks. – Brett Rigby May 30 '19 at 15:21
  • 3
    You have 2 possible cases **1)** You want to call WebAPI2 in context of the current user who logged into console app and called WebAPI1.. in this case you can use On Behalf Of flow **2)** WebAPI2 doesn’t care about the user and implicitly trusts WebAPI1.. in this case you can make use of Client Credentials Grant flow.. – Rohit Saigal May 30 '19 at 15:33
  • @RohitSaigal Thanks, but it's the calling on behalf of the user that I'm trying, and is included in the code in the question above, but I'm stuck getting it to work as although it makes a call to the second WebAPI, it doesn't appear to be correct. Or I've got it configured incorrectly, which is probably more likely. – Brett Rigby May 31 '19 at 07:07
  • @DavidTansey - Thanks for the link, but it's already been marked as being out of date, and some of the classes/methods used are marked as Obsolete. :-( The new link suggested in the post refers to the same link I posted (https://github.com/Azure-Samples/active-directory-dotnet-native-aspnetcore-v2) – Brett Rigby May 31 '19 at 08:00

1 Answers1

0

The sample you should refer to is https://github.com/Azure-Samples/active-directory-dotnet-webapp-webapi-openidconnect-aspnetcore. This sample contains a web API running on ASP.NET Core 2.0 protected by Azure AD. The web API is accessed by an ASP.NET Core 2.0 web application on behalf of the signed-in user.

The middle application in the sample is web application, not web api, but the underlying principle is same.

I would suggest you strictly follow this sample first so you understand how to call the web api 2 from web api 1 on behalf of the users. Pay attention to step 6, 7 under "Register the TodoListWebApp web application" section:

6, From the Settings blade, select Required permissions. Select + Add, and then select Select an API. 
Type TodoListService in the textbox and press Enter. Select the web API from the list and then select the Select button. Select Select Permissions. 
Tick the checkbox next to Access TodoListService and then select the Select button. Select the Done button.

7, In the Settings blade, under API Access, select Required permissions. 
Click on the Grant Permissions and when prompted press Yes. 
Once the web app is granted access to the webapi you should see the following message: Successfully granted permissions to the application for your account. 
To grant permissions for all users, please have an admin consent to the application.

The core code snippet to call web api in web application is as below:

            // Because we signed-in already in the WebApp, the userObjectId is know
            string userObjectID = (User.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier"))?.Value;

            // Using ADAL.Net, get a bearer token to access the TodoListService
            AuthenticationContext authContext = new AuthenticationContext(AzureAdOptions.Settings.Authority, new NaiveSessionCache(userObjectID, HttpContext.Session));
            ClientCredential credential = new ClientCredential(AzureAdOptions.Settings.ClientId, AzureAdOptions.Settings.ClientSecret);
            result = await authContext.AcquireTokenSilentAsync(AzureAdOptions.Settings.TodoListResourceId, credential, new UserIdentifier(userObjectID, UserIdentifierType.UniqueId));

            // Retrieve the user's To Do List.
            HttpClient client = new HttpClient();
            HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, AzureAdOptions.Settings.TodoListBaseAddress + "/api/todolist");
            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
            HttpResponseMessage response = await client.SendAsync(request);
Tom Luo
  • 602
  • 3
  • 10