microsoft-identity-web icon indicating copy to clipboard operation
microsoft-identity-web copied to clipboard

Trying to add a second MicrosoftIdentityWebAppAuthentication to log in PbiEmbed after Active Directory

Open matiasdellea27 opened this issue 3 years ago • 18 comments

Hi ! I am trying to add a second schema to succesfully log in power bi with the embed project: UserOwnsData https://docs.microsoft.com/en-us/power-bi/developer/embedded/embed-sample-for-your-organization?tabs=net-core

The project works perfectly after cloned, no problem at all, but when I try to merge with the current app of my organization it occurs that I get : System.InvalidOperationException: 'Scheme already exists: Cookies'

The app was previously working with Active Directory, and now I need to apply a new AD loggin to embed dashboards inside this app...

My Configuration method inside StartUp:

` public IConfiguration Configuration { get; }

      // This method gets called by the runtime. Use this method to add services to the container.
      public void ConfigureServices(IServiceCollection services)
      {

          // Graph api base url
          var graphBaseUrl = "https://graph.microsoft.com/v1.0";

          // Graph scope for reading logged in user's info
          var userReadScope = "user.read";


          // List of scopes required
          string[] initialScopes = new string[] { userReadScope };


          services.AddMicrosoftIdentityWebAppAuthentication(configuration: Configuration,
                                                            configSectionName: "AzureAd");

          services.AddMicrosoftIdentityWebAppAuthentication(configuration: Configuration,configSectionName: "PBIEMBED")      
                  .EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
                  .AddMicrosoftGraph(graphBaseUrl, userReadScope)
                  .AddSessionTokenCaches();

          services.AddControllersWithViews(options =>
          {
              var policy = new AuthorizationPolicyBuilder()
                  .RequireAuthenticatedUser()
                  .Build();
              options.Filters.Add(new AuthorizeFilter(policy));
          });
          services.AddRazorPages().AddMvcOptions(options =>
          {
              var policy = new AuthorizationPolicyBuilder()
                            .RequireAuthenticatedUser()
                            .Build();
              options.Filters.Add(new AuthorizeFilter(policy));
          }).AddMicrosoftIdentityUI();

          //Dependency Injection
          services.ResolveThisContext(Configuration);
          services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
          services.Configure<FormOptions>(options => options.ValueCountLimit = 5000);
          services.AddMvc(config =>
          {
              config.Filters.Add(new CustomActionFilter());
          });

          services.AddMvc()
                  .AddMvcOptions(options =>
                  {
                      options.MaxModelBindingCollectionSize = int.MaxValue;
                      options.MaxModelValidationErrors = 999999;
                  });

          services.AddDistributedMemoryCache();
          services.AddSession(options =>
          {
              options.IdleTimeout = TimeSpan.FromDays(1); // It depends on user requirements.
          });

          var config = new ConfigurationBuilder() //newForAD
             .SetBasePath(System.IO.Directory.GetCurrentDirectory())//newForAD
             .AddJsonFile("appsettings.json", false)//newForAD
             .Build();//newForAD


      }
      [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
      public class RequestFormSizeLimitAttribute : Attribute, IAuthorizationFilter, IOrderedFilter
      {
          private readonly FormOptions _formOptions;

          public RequestFormSizeLimitAttribute(int valueCountLimit)
          {
              _formOptions = new FormOptions()
              {
                  ValueCountLimit = valueCountLimit
              };
          }

          public int Order { get; set; }

          public void OnAuthorization(AuthorizationFilterContext context)
          {
              var features = context.HttpContext.Features;
              var formFeature = features.Get<IFormFeature>();

              if (formFeature == null || formFeature.Form == null)
              {
                  // Request form has not been read yet, so set the limits
                  features.Set<IFormFeature>(new FormFeature(context.HttpContext.Request, _formOptions));
              }
          }
      }
      // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
      public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
      {
          if (env.IsDevelopment())
          {
              app.UseDeveloperExceptionPage();
          }
          else
          {
              app.UseExceptionHandler("/Home/Error");
              // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
              app.UseHsts();
          }
          app.UseHttpsRedirection();
          app.UseStaticFiles();
          
          app.UseRouting();
          app.UseCookiePolicy();
          app.UseAuthentication();
          app.UseAuthorization();
          app.UseSession(); //cambio
          app.UseEndpoints(endpoints =>
          {
              endpoints.MapControllerRoute(
                  name: "default",
                  pattern: "{controller=Home}/{action=Index}/{id?}");
              //endpoints.MapRazorPages();
          });
      }
  }
  

} `

MY app settings with the double json, one for each log in config:

` //Active Directory "AzureAd": { "Instance": "https://login.microsoftonline.com", "Domain": "xxxxxxxxxx", "TenantId": "xxxxxxxxxxx", "ClientId": "xxxxxxxxxxx", "CallbackPath": "/signin-oidc", "SignedOutCallbackPath": "/signout-oidc" },

//PBI EMBED USER OWNS DATA "PBIEMBED": { "Instance": "https://login.microsoftonline.com/", "Domain": "https://api.powerbi.com", "TenantId": "xxxxxxxxxxxxxx", "ClientId": "xxxxxxxxxxxxxx", "ClientSecret": "xxxxxxxxxxxxxxxxx", //"CallbackPath": "/signin-oidc2" //"SignedOutCallbackPath": "/signout-oidc" }, "PowerBiHostname": "https://app.powerbi.com"`

I tried to do with all of different parameters...but nothing worked well :(

matiasdellea27 avatar Aug 04 '21 21:08 matiasdellea27

@matiasdellea27 have you seen our documentation on using multiple-auth schemes? You'll need to define a separate cookie scheme for one of the auth schemes.

jennyf19 avatar Aug 05 '21 00:08 jennyf19

I am trying all that I can! but not success :(

services.AddAuthentication() .AddMicrosoftIdentityWebApp(Configuration.GetSection("AzureAd"), Microsoft.Identity.Web.Constants.AzureAd, null);

and:

services.AddAuthentication() .AddMicrosoftIdentityWebApp(Configuration.GetSection("PBIEMBED"), Microsoft.Identity.Web.Constants.AzureAdB2C, null) .EnableTokenAcquisitionToCallDownstreamApi(initialScopes) .AddMicrosoftGraph(graphBaseUrl, userReadScope) .AddSessionTokenCaches();

But this config gives me the not null name error... but trying to add a name is worthless

matiasdellea27 avatar Aug 05 '21 05:08 matiasdellea27

InvalidOperationException: No authenticationScheme was specified, and there was no DefaultChallengeScheme found. The default schemes can be set using either AddAuthentication(string defaultScheme) or AddAuthentication(Action<AuthenticationOptions> configureOptions).

This is a new error after updating both dependencies to the last version: 1.15.2 ( Identity.Web.UI and Identity.Web.MicrosoftGraph )

My new config in start up is this:


            services.AddAuthentication()
                    .AddMicrosoftIdentityWebApp(Configuration.GetSection("AzureAd"), Microsoft.Identity.Web.Constants.AzureAd, null);

            services.AddAuthentication()
                   .AddMicrosoftIdentityWebApp(Configuration.GetSection("PBIEMBED"), Microsoft.Identity.Web.Constants.AzureAdB2C, null)
                    .EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
                    .AddMicrosoftGraph(graphBaseUrl, userReadScope)
                    .AddSessionTokenCaches();

matiasdellea27 avatar Aug 05 '21 12:08 matiasdellea27

I take this opportunity to ask you, and I take advantage of your generosity jennyf19, is it possible to generate a general authentication for the two schemes when entering the application, and that when I reach the PBI request, the user is ALREADY logged in? since the client_id is the same! and both apps run on the same azure, even though they are different applications.

matiasdellea27 avatar Aug 05 '21 13:08 matiasdellea27

@matiasdellea27 The first services.AddAuthentication(Microsoft.Identity.Web.Constants.AzureAd) needs to define the default scheme.

A second issue is you cannot call graph w/B2C, so you would need to move this:

.EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
                    .AddMicrosoftGraph(graphBaseUrl, userReadScope)
                    .AddSessionTokenCaches();

to under the AzureAd section.

jennyf19 avatar Aug 05 '21 20:08 jennyf19

These 3 lines are coming in the microsoft, power bi embed project... that's why I have them !

I will try to fix it! thank you very much!!!

matiasdellea27 avatar Aug 05 '21 21:08 matiasdellea27

Thank you @jennyf19 ! this is now working for the first authentication in active directory, but failing causing the token to be null at the second instance when I try to log in power bi...

` // Graph api base url var graphBaseUrl = "https://graph.microsoft.com/v1.0";

        // Graph scope for reading logged in user's info
        var userReadScope = "user.read";


        // List of scopes required
        string[] initialScopes = new string[] { userReadScope };

        //original AD
        //services.AddMicrosoftIdentityWebAppAuthentication(configuration: Configuration,
        //                                                  configSectionName: "AzureAd", Microsoft.Identity.Web.Constants.AzureAd);


        services.AddAuthentication(Microsoft.Identity.Web.Constants.AzureAd)
                .AddMicrosoftIdentityWebApp(Configuration.GetSection("AzureAd"), Microsoft.Identity.Web.Constants.AzureAd, "cookiesAd");

        services.AddAuthentication()
                           .AddMicrosoftIdentityWebApp(Configuration.GetSection("PBIEMBED"), Microsoft.Identity.Web.Constants.Scope, "cookiesPBI")
                            .EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
                            .AddMicrosoftGraph(graphBaseUrl, userReadScope)
                            .AddSessionTokenCaches();`
                            
                            
                            

I did something wrong by using ".scope" at the second authentication ?

matiasdellea27 avatar Aug 05 '21 22:08 matiasdellea27

image

I am having this error now! The first log in into the app is now working succesfully !

I know that you are saying that I am not supposed to use Graph,,, but the code uses it! and it is working in the cloned project: https://github.com/microsoft/PowerBI-Developer-Samples

matiasdellea27 avatar Aug 06 '21 05:08 matiasdellea27

Or maybe there is a way to keep logged to AD and then go into power bi with the same token ( ClientID-TenantId-Secret) are the same in both loggings !!!

matiasdellea27 avatar Aug 06 '21 06:08 matiasdellea27

@matiasdellea27 you don't need two auth schemes. just use the one and use the OpenIdConnect default. I would just use our basic web app calls web api template and go from there.

mkdir webapp-calls-api
cd webapp-calls-api
dotnet new webapp2--auth  SingleOrg --called-api-url "https://app.powerbi.com" --called-api-scopes "PowerBiScopes.ReadDashBoard"

You can also include MicrosoftGraph, but you don't need a separate section and separate auth scheme.

jennyf19 avatar Aug 06 '21 16:08 jennyf19

Hi Again @jennyf19 ! I have been told to use this method: https://docs.microsoft.com/en-us/power-bi/developer/embedded/embedded-faq#my-application-already-uses-aad-for-user-authentication--how-can-we-use-this-identity-when-authenticating-to-power-bi-in-a--user-owns-data--scenario-

"Once you have a user token to your app, you simply call to ADAL API AcquireTokenAsync using the user access token and specify the Power BI resource URL as the resource ID"

In this steps I am wondering where to obtain the token ? because after the succesfull log in I am not able to retrieve the token in a variable to send to the PBIEmbed controller... can you help me with this ?

matiasdellea27 avatar Aug 09 '21 21:08 matiasdellea27

@matiasdellea27 ADAL is on the deprecation path, so that doc needs to be updated. Who told you to use this?

jennyf19 avatar Aug 09 '21 23:08 jennyf19

Some guy in stackoverflow :(

matiasdellea27 avatar Aug 10 '21 01:08 matiasdellea27

so, that's why there is no way to get the token as a variable ?

matiasdellea27 avatar Aug 10 '21 01:08 matiasdellea27

This is the controller in charge of reveiving the token authenticated from power bi ( or azure ad )

`// ---------------------------------------------------------------------------- // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. // ----------------------------------------------------------------------------

namespace namespace { using NFP.Domain.PbiEmbedUOD; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Identity.Web; using Microsoft.Graph; using System.Threading.Tasks;

[Authorize]
public class ReportsController : Controller
{
    private readonly GraphServiceClient m_graphServiceClient;

    private readonly ITokenAcquisition m_tokenAcquisition;

    public ReportsController(ITokenAcquisition tokenAcquisition,
                          GraphServiceClient graphServiceClient)
    {
        this.m_tokenAcquisition = tokenAcquisition;
        this.m_graphServiceClient = graphServiceClient;
    }

    public IActionResult Index()
    {
        return View();
    }

    // Redirects to login page to request increment consent
    [AuthorizeForScopes(Scopes = new string[] { PowerBiScopes.ReadDashboard, PowerBiScopes.ReadReport, PowerBiScopes.ReadWorkspace } )]
    public async Task<IActionResult> Embed()
    {
        // Generate token for the signed in user
        var accessToken = await m_tokenAcquisition.GetAccessTokenForUserAsync(new string[] { PowerBiScopes.ReadDashboard, PowerBiScopes.ReadReport, PowerBiScopes.ReadWorkspace });

        // Get username of logged in user

        var userInfo = await m_graphServiceClient.Me.Request().GetAsync();
        var userName = userInfo.DisplayName;

        AuthDetails authDetails = new AuthDetails
        {
            UserName = userName,
            AccessToken = accessToken
        };

        return View(authDetails);
    }
}

} `

It is exactly the same controller provided by UserOwnsData project downloaded from here: https://github.com/microsoft/PowerBI-Developer-Samples/tree/master/.NET%20Core/Embed%20for%20your%20organization

matiasdellea27 avatar Aug 10 '21 04:08 matiasdellea27

Hi ! @jennyf19 , can you confirm to me if this method is working ?

https://docs.microsoft.com/en-us/dotnet/api/microsoft.identitymodel.clients.activedirectory.authenticationcontext.acquiretokenasync?view=azure-dotnet

Maybe I can retrieve the token in this method

matiasdellea27 avatar Aug 10 '21 18:08 matiasdellea27

@matiasdellea27 : you should not use ADAL.NET

jmprieur avatar Aug 21 '22 19:08 jmprieur

In your ReportsController code, why do you acquire a token for PowerBI, if you want to call Graph?

jmprieur avatar Aug 21 '22 19:08 jmprieur