AzureAD / microsoft-identity-web

Helps creating protected web apps and web APIs with Microsoft identity platform and Azure AD B2C
MIT License
679 stars 209 forks source link

TokenAcquirerFactory.GetDefaultInstance<OwinTokenAcquirerFactory>() raises System.NullReferenceException #2680

Open rose-alex42 opened 7 months ago

rose-alex42 commented 7 months ago

Microsoft.Identity.Web Library

Microsoft.Identity.Web.OWIN

Microsoft.Identity.Web version

2.17.0

Web app

Not Applicable

Web API

Protected web APIs (validating scopes/roles)

Token cache serialization

In-memory caches

Description

Following documentation on how to setup with OWIN Web API, I tried to adapt the solution to work with a self-hosted OWIN setup, but I get a System.NullReferenceException when I try to call TokenAcquirerFactory .GetDefaultInstance <OwinTokenAcquirerFactory>(). The setup is explained in the reproduction steps, but basically it calls Microsoft.Owin.Hosting.WebApp.Start<Startup>() in a console app (which works fine if I don't try to call the OwinTokenAcquirerFactory).

It throws an exception in the DefineConfiguration method override, which binds default AzureAD appsettings values and, for some reason, returns HttpContext.Current.Request.PhysicalApplicationPath

        protected override string DefineConfiguration(IConfigurationBuilder builder)
        {
            _ = builder.AddInMemoryCollection(new Dictionary<string, string>()
            {
                ["AzureAd:Instance"] = EnsureTrailingSlash(ConfigurationManager.AppSettings["ida:Instance"] ?? ConfigurationManager.AppSettings["ida:AADInstance"] ?? "https://login.microsoftonline.com/"),
                ["AzureAd:ClientId"] = ConfigurationManager.AppSettings["ida:ClientId"],
                ["AzureAd:TenantId"] = ConfigurationManager.AppSettings["ida:Tenant"] ?? ConfigurationManager.AppSettings["ida:TenantId"],
                ["AzureAd:Audience"] = ConfigurationManager.AppSettings["ida:Audience"],
                ["AzureAd:ClientSecret"] = ConfigurationManager.AppSettings["ida:ClientSecret"],
                ["AzureAd:SignedOutCallbackPath"] = ConfigurationManager.AppSettings["ida:PostLogoutRedirectUri"],
                ["AzureAd:RedirectUri"] = ConfigurationManager.AppSettings["ida:RedirectUri"],
            });
            return HttpContext.Current.Request.PhysicalApplicationPath; // why?
        }

Considering that I'm in a Console app, and just starting up the owin host, there are no HttpContext. I tried assigning a bogus HttpContext to Current but it still complained about path being null further down the line. Also, I made sure to run Visual Studio in Administrator too, as it crashed around FilePermissions when using the bogus HttpContext.

Reproduction steps

  1. Create a new .NET Framework 4.8 Console App
  2. Add the Microsoft.Identity.Web.OWIN, Microsoft.AspNet.WebApi.OwinSelfHost and Microsoft.Owin.Security.Cookies
  3. Add a Startup.cs file and copy this code

    public partial class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            // Web API configuration and services
            var config = new HttpConfiguration();
            // Web API routes
            config.MapHttpAttributeRoutes();
    
            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
    
            app.UseWebApi(config);
    
            app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
            app.UseCookieAuthentication(new CookieAuthenticationOptions());
    
            OwinTokenAcquirerFactory factory = TokenAcquirerFactory.GetDefaultInstance<OwinTokenAcquirerFactory>();
            app.AddMicrosoftIdentityWebApi(factory);
            factory.Build();
        }
    }
  4. Add this bit of code to your Main
            var baseAddress = "http://localhost:9000/";
            using (var server = WebApp.Start<Startup>(baseAddress))
            {
                Console.WriteLine(string.Format("Server running at {0}", baseAddress));
                Console.ReadLine();
            }
  5. Add the following to your App.config
    <appSettings>
        <add key="ida:ClientId" value="your-client-id" />
        <add key="ida:AADInstance" value="https://login.microsoftonline.com/" />
        <add key="ida:Domain" value="yourdomain.com" />
        <add key="ida:TenantId" value="your-tenant-id" />
        <add key="ida:PostLogoutRedirectUri" value="https://localhost:PORT/signin-oidc" />
        <add key="ida:ClientSecret" value="your-client-secret" />
    </appSettings>
  6. Add an appsettings.json file at the root of your project, make it Copy Always and put the following content:
    {
    "AzureAD": {
    "Instance": "https://login.microsoftonline.com/",
    "ClientId": "your-client-id",
    "TenantId": "common",
    "ClientCredentials": [
      {
        "SourceType": "ClientSecret",
        "ClientSecret": "your-client-secret"
      }
    ]
    }
    }
  7. Start your application

Error message

System.NullReferenceException HResult=0x80004003 Message=Object reference not set to an instance of an object. Source=Microsoft.Identity.Web.OWIN StackTrace: at Microsoft.Identity.Web.OWIN.OwinTokenAcquirerFactory.DefineConfiguration(IConfigurationBuilder builder) at Microsoft.Identity.Web.TokenAcquirerFactory.ReadConfiguration() at Microsoft.Identity.Web.TokenAcquirerFactory.GetDefaultInstance[T](String configSection) at MicrosoftEntraSelfHostOWIN.Startup.Configuration(IAppBuilder app) in C:\GitHub\MicrosoftEntraOWIN\MicrosoftEntraSelfHostOWIN\Startup.cs:line 30

Id Web logs

No response

Relevant code snippets

public partial class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            // Web API configuration and services
            var config = new HttpConfiguration();
            // Web API routes
            config.MapHttpAttributeRoutes();

            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );

            app.UseWebApi(config);

            app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
            app.UseCookieAuthentication(new CookieAuthenticationOptions());

            OwinTokenAcquirerFactory factory = TokenAcquirerFactory.GetDefaultInstance<OwinTokenAcquirerFactory>();
            app.AddMicrosoftIdentityWebApi(factory);
            factory.Build();
        }
    }

    internal class Program
    {
        static void Main(string[] args)
        {
            var baseAddress = "http://localhost:9000/";
            using (var server = WebApp.Start<Startup>(baseAddress))
            {
                Console.WriteLine(string.Format("Server running at {0}", baseAddress));
                Console.ReadLine();
            }
        }
    }

    <appSettings>
        <add key="ida:ClientId" value="ba39bc6d-30c9-4882-9d7f-7aa94fa16afc" />
        <add key="ida:AADInstance" value="https://login.microsoftonline.com/" />
        <add key="ida:Domain" value="indixio.com" />
        <add key="ida:TenantId" value="bda58cbe-3512-4e4b-b8e2-7c3972ce7116" />
        <add key="ida:PostLogoutRedirectUri" value="https://localhost:44382/signin-oidc" />
        <add key="ida:ClientSecret" value="" />
    </appSettings>

Regression

No response

Expected behavior

I expect TokenAcquirerFactory.GetDefaultInstance<OwinTokenAcquirerFactory>() to return an OwinTokenAcquirerFactory, even when in a Self-Hosted Owin environment.

I can also live with documented extra steps for this specific scenario.

jennyf19 commented 6 months ago

@jmprieur FYI.