Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Null Reference Exception | PostConfigureApplicationCookieTicketStore #1522

Open
kyle-sexton opened this issue Dec 19, 2024 · 1 comment
Open

Comments

@kyle-sexton
Copy link

kyle-sexton commented Dec 19, 2024

Which version of Duende IdentityServer are you using?
7.1.0-rc.1

Which version of .NET are you using?
.NET 9

Describe the bug
After upgrading I am experiencing a null reference exception on startup. After looking at the changes, and debugging the PostConfigureApplicationCookieTicketStore locally I can see that the _licenseUsage is set to null in the constructor due to the httpContextAccessor being null. I do have the line in service registration that adds the HttpContextAccessor to the service provider, but am assuming this is null due to the the constructor being executed shortly after app.RunAsync() is called and there is no active HTTP request yet.

public PostConfigureApplicationCookieTicketStore(
    IHttpContextAccessor httpContextAccessor,
    IdentityServerOptions identityServerOptions,
    IOptions<Microsoft.AspNetCore.Authentication.AuthenticationOptions> options,
    ILogger<PostConfigureApplicationCookieTicketStore> logger)
{
    // The HTTP context accessor is null, and thus _licenseUsage is null. The constructor is only called once.
    _httpContextAccessor = httpContextAccessor;
    _licenseUsage = httpContextAccessor.HttpContext?.RequestServices.GetRequiredService<LicenseUsageTracker>();
    _logger = logger;

    _scheme = identityServerOptions.Authentication.CookieAuthenticationScheme ??
         options.Value.DefaultAuthenticateScheme ??
         options.Value.DefaultScheme;
}

To Reproduce

Upgrade to 7.1.0-rc.1, fix compile errors (if any) and startup the application. I have a combination of the Entity Framework and ASP.NET Identity quickstart.

Expected behavior

Expected app to start up without errors, and allow navigation around the home/index page, login page, and all other ASP.NET identity related pages.

Log output/exception with stacktrace

An unhandled exception occurred while processing the request.
NullReferenceException: Object reference not set to an instance of an object.
Duende.IdentityServer.Configuration.PostConfigureApplicationCookieTicketStore.PostConfigure(string name, CookieAuthenticationOptions options) in PostConfigureApplicationCookieTicketStore.cs, line 72

NullReferenceException: Object reference not set to an instance of an object.
Duende.IdentityServer.Configuration.PostConfigureApplicationCookieTicketStore.PostConfigure(string name, CookieAuthenticationOptions options) in PostConfigureApplicationCookieTicketStore.cs
Microsoft.Extensions.Options.OptionsFactory<TOptions>.Create(string name)
System.Lazy<T>.ViaFactory(LazyThreadSafetyMode mode)
System.Lazy<T>.ExecutionAndPublication(LazyHelper executionAndPublication, bool useDefaultConstructor)
System.Lazy<T>.CreateValue()
Microsoft.Extensions.Options.OptionsCache<TOptions>.GetOrAdd<TArg>(string name, Func<string, TArg, TOptions> createOptions, TArg factoryArgument)
Microsoft.Extensions.Options.OptionsMonitor<TOptions>.Get(string name)
Microsoft.AspNetCore.Authentication.AuthenticationHandler<TOptions>.InitializeAsync(AuthenticationScheme scheme, HttpContext context)
Microsoft.AspNetCore.Authentication.AuthenticationHandlerProvider.GetHandlerAsync(HttpContext context, string authenticationScheme)
Duende.IdentityServer.Hosting.FederatedSignOut.FederatedSignoutAuthenticationHandlerProvider.GetHandlerAsync(HttpContext context, string authenticationScheme) in FederatedSignoutAuthenticationHandlerProvider.cs
Microsoft.AspNetCore.Authentication.AuthenticationService.AuthenticateAsync(HttpContext context, string scheme)
Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
Duende.IdentityServer.Hosting.DynamicProviders.DynamicSchemeAuthenticationMiddleware.Invoke(HttpContext context) in DynamicSchemeAuthenticationMiddleware.cs
Duende.IdentityServer.Hosting.BaseUrlMiddleware.Invoke(HttpContext context) in BaseUrlMiddleware.cs
Enterprise.Applications.AspNetCore.Middleware.IgnoreFavicon.IgnoreFaviconMiddleware.InvokeAsync(HttpContext context, RequestDelegate next)
Microsoft.AspNetCore.Builder.UseMiddlewareExtensions+InterfaceMiddlewareBinder+<>c__DisplayClass2_0+<<CreateMiddleware>b__0>d.MoveNext()
Swashbuckle.AspNetCore.ReDoc.ReDocMiddleware.Invoke(HttpContext httpContext)
Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)
Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
Microsoft.AspNetCore.Builder.UseMiddlewareExtensions+InterfaceMiddlewareBinder+<>c__DisplayClass2_0+<<CreateMiddleware>b__0>d.MoveNext()
Enterprise.Middleware.AspNetCore.ConfigurableOptions.ConfigurableOptionsEndpointMiddleware.InvokeAsync(HttpContext context, RequestDelegate next)
Microsoft.AspNetCore.Builder.UseMiddlewareExtensions+InterfaceMiddlewareBinder+<>c__DisplayClass2_0+<<CreateMiddleware>b__0>d.MoveNext()
Enterprise.Middleware.AspNetCore.RegisteredServices.RegisteredServicesEndpointMiddleware.InvokeAsync(HttpContext context, RequestDelegate next)
Microsoft.AspNetCore.Builder.UseMiddlewareExtensions+InterfaceMiddlewareBinder+<>c__DisplayClass2_0+<<CreateMiddleware>b__0>d.MoveNext()
Microsoft.AspNetCore.Diagnostics.StatusCodePagesMiddleware.Invoke(HttpContext context)
Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)
System.NullReferenceException: Object reference not set to an instance of an object.
   at Duende.IdentityServer.Configuration.PostConfigureApplicationCookieTicketStore.PostConfigure(String name, CookieAuthenticationOptions options) in /_/src/IdentityServer/Configuration/DependencyInjection/PostConfigureApplicationCookieTicketStore.cs:line 72
   at Microsoft.Extensions.Options.OptionsFactory`1.Create(String name)
   at System.Lazy`1.ViaFactory(LazyThreadSafetyMode mode)
   at System.Lazy`1.ExecutionAndPublication(LazyHelper executionAndPublication, Boolean useDefaultConstructor)
   at System.Lazy`1.CreateValue()
   at Microsoft.Extensions.Options.OptionsCache`1.GetOrAdd[TArg](String name, Func`3 createOptions, TArg factoryArgument)
   at Microsoft.Extensions.Options.OptionsMonitor`1.Get(String name)
   at Microsoft.AspNetCore.Authentication.AuthenticationHandler`1.InitializeAsync(AuthenticationScheme scheme, HttpContext context)
   at Microsoft.AspNetCore.Authentication.AuthenticationHandlerProvider.GetHandlerAsync(HttpContext context, String authenticationScheme)
   at Duende.IdentityServer.Hosting.FederatedSignOut.FederatedSignoutAuthenticationHandlerProvider.GetHandlerAsync(HttpContext context, String authenticationScheme) in /_/src/IdentityServer/Hosting/FederatedSignOut/FederatedSignoutAuthenticationHandlerProvider.cs:line 33
   at Microsoft.AspNetCore.Authentication.AuthenticationService.AuthenticateAsync(HttpContext context, String scheme)
   at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
   at Duende.IdentityServer.Hosting.DynamicProviders.DynamicSchemeAuthenticationMiddleware.Invoke(HttpContext context) in /_/src/IdentityServer/Hosting/DynamicProviders/DynamicSchemes/DynamicSchemeAuthenticationMiddleware.cs:line 51
   at Duende.IdentityServer.Hosting.BaseUrlMiddleware.Invoke(HttpContext context) in /_/src/IdentityServer/Hosting/BaseUrlMiddleware.cs:line 27
   at Enterprise.Applications.AspNetCore.Middleware.IgnoreFavicon.IgnoreFaviconMiddleware.InvokeAsync(HttpContext context, RequestDelegate next)
   at Microsoft.AspNetCore.Builder.UseMiddlewareExtensions.InterfaceMiddlewareBinder.<>c__DisplayClass2_0.<<CreateMiddleware>b__0>d.MoveNext()
--- End of stack trace from previous location ---
   at Swashbuckle.AspNetCore.ReDoc.ReDocMiddleware.Invoke(HttpContext httpContext)
   at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)
   at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
   at Microsoft.AspNetCore.Builder.UseMiddlewareExtensions.InterfaceMiddlewareBinder.<>c__DisplayClass2_0.<<CreateMiddleware>b__0>d.MoveNext()
--- End of stack trace from previous location ---
   at Enterprise.Middleware.AspNetCore.ConfigurableOptions.ConfigurableOptionsEndpointMiddleware.InvokeAsync(HttpContext context, RequestDelegate next)
   at Microsoft.AspNetCore.Builder.UseMiddlewareExtensions.InterfaceMiddlewareBinder.<>c__DisplayClass2_0.<<CreateMiddleware>b__0>d.MoveNext()
--- End of stack trace from previous location ---
   at Enterprise.Middleware.AspNetCore.RegisteredServices.RegisteredServicesEndpointMiddleware.InvokeAsync(HttpContext context, RequestDelegate next)
   at Microsoft.AspNetCore.Builder.UseMiddlewareExtensions.InterfaceMiddlewareBinder.<>c__DisplayClass2_0.<<CreateMiddleware>b__0>d.MoveNext()
--- End of stack trace from previous location ---
   at Microsoft.AspNetCore.Diagnostics.StatusCodePagesMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)

Additional context

I have gone through the breaking changes section in the release and updated the constructor of my custom / extended configuration store DB context class, and namespaces from IdentityModel to Duende.IdentityModel. I haven't changed anything else except for the other Duende packages for ASP.NET Identity and Entity Framework Core.

I do have some additional cookie option related calls in my code such as services.ConfigureApplicationCookie(). I've tried commenting these out but they haven't seemed to have an effect. You might notice the other middleware listed in the stack trace. Those are only enabled in the dev environment and are mapped to specific paths that aren't being hit.

Hope this helps. I may have something weird in my startup code that is affecting it, but figured I'd create an issue and start the discussion. Its worth noting that the previous 7.0.8 version had worked just fine, and I haven't made many major changes to the code. Let me know if you need any additional information.

@kyle-sexton
Copy link
Author

kyle-sexton commented Dec 26, 2024

Everything seems to work fine as soon as I comment out the .AddServerSideSessions() call on the IIdentityServerBuilder. My identity server registration has some extra stuff I had experimented with, but those don't seem to have any impact. I do have custom / derived DbContexts for both the configuration and operational data for future extensibility reasons. Its the same approach I've seen suggested with the ASP.NET Identity db context.

string? connectionString = configuration.GetConnectionString(ConnectionStringKeys.IdentityServer);

IIdentityServerBuilder idsrvBuilder = services
    .AddIdentityServer(options =>
    {
        // https://docs.duendesoftware.com/identityserver/v7/reference/options

        options.Authentication.CookieAuthenticationScheme = CookieSchemes.CookieAuthenticationScheme;

        // https://docs.duendesoftware.com/identityserver/v7/ui/logout/session_cleanup
        // This will remove long-lived tokens (refresh tokens) associated with the user when they log out.
        options.Authentication.CoordinateClientLifetimesWithUserSession = true;

        options.EmitScopesAsSpaceDelimitedStringInJwt = true;

        // Emits a static claim with the format {issuer}/resources.
        // Example: https://identity.example.com/resources
        // NOTE: Both the static audience and audiences from API resources can be used.
        options.EmitStaticAudienceClaim = true;

        // https://docs.duendesoftware.com/identityserver/v7/tokens/par
        // https://github.com/DuendeSoftware/Samples/tree/main/IdentityServer/v7/Basics/MvcPar
        options.Endpoints.EnablePushedAuthorizationEndpoint = true;

        options.Events.RaiseSuccessEvents = true;
        options.Events.RaiseFailureEvents = true;
        options.Events.RaiseErrorEvents = true;
        options.Events.RaiseInformationEvents = true;

        // https://docs.duendesoftware.com/identityserver/v7/fundamentals/keys/automatic_key_management
        options.KeyManagement.Enabled = true;
        options.KeyManagement.RotationInterval = TimeSpan.FromDays(90);
        options.KeyManagement.PropagationTime = TimeSpan.FromDays(14);
        options.KeyManagement.RetentionDuration = TimeSpan.FromDays(14);
        options.KeyManagement.DeleteRetiredKeys = true;

        // TODO: Add this when ready to go live in production.
        // We will need a community license from Duende (send email).
        //options.LicenseKey = "";

        if (environment.IsDevelopment())
        {
            // Claim type used for the user’s display name. Unset by default due to possible PII concerns.
            // If used, this would commonly be JwtClaimTypes.Name, JwtClaimType.Email or a custom claim.
            options.ServerSideSessions.UserDisplayNameClaimType = JwtClaimTypes.Email;

            // https://docs.duendesoftware.com/identityserver/v7/ui/server_side_sessions/session_expiration
            options.ServerSideSessions.RemoveExpiredSessionsFrequency = TimeSpan.FromMinutes(10);

            // https://docs.duendesoftware.com/identityserver/v7/ui/server_side_sessions/inactivity_timeout
            options.ServerSideSessions.ExpiredSessionsTriggerBackchannelLogout = true;
        }

        options.UserInteraction.LoginUrl = PathConstants.LoginPath;
        options.UserInteraction.LogoutUrl = PathConstants.LogoutPath;
        options.UserInteraction.ConsentUrl = PathConstants.ConsentPath;
        options.UserInteraction.ErrorUrl = PathConstants.ErrorPath;
        options.UserInteraction.DeviceVerificationUrl = PathConstants.DeviceVerificationPath;
        options.UserInteraction.CreateAccountUrl = PathConstants.RegisterPath;

        // https://github.com/DuendeSoftware/IdentityServer/releases/tag/7.0.7
        //options.UserInteraction.PromptValuesSupported.Add("custom-prompt");
    })
    .AddServerSideSessions()
    //.AddInMemoryIdentityResources(Config.IdentityResources)
    //.AddInMemoryApiScopes(Config.ApiScopes)
    //.AddInMemoryClients(Config.Clients)
    // This adds the config data from DB (clients, resources, CORS).
    .AddConfigurationStore<CustomConfigurationDbContext>(options =>
    {
        //options.DefaultSchema = Schemas.Config;

        options.ConfigureDbContext = b =>
            b.UseSqlServer(connectionString,
                dbOpts =>
                {
                    dbOpts.MigrationsAssembly(AssemblyReference.Assembly.FullName);
                    dbOpts.EnableRetryOnFailure();
                });
    })
    // https://docs.duendesoftware.com/identityserver/v7/data/ef/#enabling-caching-for-configuration-store
    // This should be enabled in production to reduce database load (request volume).
    // TODO: It might be better to use a distributed cache instead of in-memory.
    .AddConfigurationStoreCache()
    // This adds the operational data from DB (codes, tokens, consents).
    .AddOperationalStore<CustomPersistedGrantDbContext>(options =>
    {
        //options.DefaultSchema = Schemas.PersistedGrant;

        options.ConfigureDbContext = b =>
            b.UseSqlServer(connectionString,
                dbOpts =>
                {
                    dbOpts.MigrationsAssembly(AssemblyReference.Assembly.FullName);
                    dbOpts.EnableRetryOnFailure();
                });

        // This enables automatic token cleanup (optional).
        options.EnableTokenCleanup = true;
        options.TokenCleanupInterval = 3600; // Cleanup expired tokens once per hour.
    })
    //.AddTestUsers(TestUsers.Users)
    .AddAspNetIdentity<ApplicationUser>()
    .AddAuthorizeInteractionResponseGenerator<CustomAuthorizeInteractionResponseGenerator>()
    .AddProfileService<CustomProfileService>()
    .AddBackchannelAuthenticationUserValidator<BackChannelAuthenticationUserValidator>()
    .AddBackchannelAuthenticationUserNotificationService<BackchannelAuthenticationUserNotificationService>()
    .AddLicenseSummary();

// https://blog.duendesoftware.com/posts/20220406_session_management
// https://docs.duendesoftware.com/identityserver/v7/ui/server_side_sessions
//idsrvBuilder.AddServerSideSessions();

// https://docs.duendesoftware.com/identityserver/v7/ui/login/external
// Configures the OpenIdConnect handlers to persist the state parameter into the server-side IDistributedCache.
// This requires that an implementation of IDistributedCache has been registered.
services.AddOidcStateDataFormatterCache();

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant