Search code examples
c#azureauthorizationidentityclaims

How do I call a web API on behalf of an AAD user using security claims?


So, to give you a general overview of what I am trying to achieve, I have a web application which uses AAD authentication, and so users need to be signed in to a Microsoft organizational account in order to use most of the controllers implemented in the web app (which targets .NET Core). Visual Studio offers a template for this kind of web app setup. This template project seems to obtain the user’s identity as a "ClaimsIdentity" (System.Security.Claims.ClaimsIdentity), which is ok so far, as long as the user is AAD authenticated. I also have a .NET Core Web API solution which the web app needs to make calls to on behalf of the logged in user. So, I have a web app which signs in the user to AAD and then a web API (which the web app calls) which has controller end points that expect an AAD authenticated request. For this to work, my understanding is that the web app needs to include the signed in identity that Microsoft, (which in this case is the security provider) provided it with, inside the header of the request that it makes to the API. The API would then be able to view user claims and act accordingly.

The problem is here. As a header, I believe I need to provide the access token that Microsoft sends to the web app.. however I cannot locate this token. All I can extract from User or User.Identity, are the claims. How can I call a separate API on behalf of these claims? Do I need to completely disregard the template that Microsoft provided and just make a call to the /token endpoint? I would just like to do this the right way :)

This is the ConfigureServices method in the web app Startup class:

public void ConfigureServices(IServiceCollection services)
    {
        services.Configure<CookiePolicyOptions>(options =>
        {
            // This lambda determines whether user consent for non-essential cookies is needed for a given request.
            options.CheckConsentNeeded = context => true;
            options.MinimumSameSitePolicy = SameSiteMode.None;
        });

        services.AddAuthentication(AzureADDefaults.AuthenticationScheme)
            .AddAzureAD(options => Configuration.Bind("AzureAd", options));

        services.AddMvc(options =>
        {
            var policy = new AuthorizationPolicyBuilder()
                .RequireAuthenticatedUser()
                .Build();
            options.Filters.Add(new AuthorizeFilter(policy));
        })
        .SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
    }

This is where I would like to call the external web API on behalf of the logged in AAD to get the required data:

public IActionResult Index()
    {
        var user = User.Identity as ClaimsIdentity;

        var request = (HttpWebRequest)WebRequest.Create("http://localhost:4110/data");
        request.Headers["Authorization"] = "bearer " + getAccessToken_using_user;

        var response = (HttpWebResponse)request.GetResponse();

        var dataString = new StreamReader(response.GetResponseStream()).ReadToEnd();

        return View();
}

Of course, my intention is to replace "getAccessToken_using_user" with the access token that Microsoft supposedly provides the web app with, as illustrated in their diagram.


Solution

  • You can use MSAL to get the access token for the downstream API.

    https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/on-behalf-of#practical-usage-of-obo-in-an-aspnet--aspnet-core-application

    This is a full example with on behalf flow:

    https://github.com/Azure-Samples/active-directory-dotnet-native-aspnetcore-v2/tree/master/2.%20Web%20API%20now%20calls%20Microsoft%20Graph

    public static IServiceCollection AddProtectedApiCallsWebApis(this IServiceCollection services, IConfiguration configuration, IEnumerable<string> scopes)
    {
     ...
     services.Configure<JwtBearerOptions>(AzureADDefaults.JwtBearerAuthenticationScheme, options =>
     {
      options.Events.OnTokenValidated = async context =>
      {
       var tokenAcquisition = context.HttpContext.RequestServices.GetRequiredService<ITokenAcquisition>();
       context.Success();
    
       // Adds the token to the cache, and also handles the incremental consent and claim challenges
       tokenAcquisition.AddAccountToCacheFromJwt(context, scopes);
       await Task.FromResult(0);
      };
     });
     return services;
    }
    
    private async Task GetTodoList(bool isAppStarting)
    {
     ...
     //
     // Get an access token to call the To Do service.
     //
     AuthenticationResult result = null;
     try
     {
      result = await _app.AcquireTokenSilent(Scopes, accounts.FirstOrDefault())
                         .ExecuteAsync()
                         .ConfigureAwait(false);
     }
    ...
    
    // Once the token has been returned by MSAL, add it to the http authorization header, before making the call to access the To Do list service.
    _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
    
    // Call the To Do list service.
    HttpResponseMessage response = await _httpClient.GetAsync(TodoListBaseAddress + "/api/todolist");
    ...
    }