Logout does not seem to be working correctly

Michael Mastro II 51 Reputation points
2022-07-06T11:42:24.587+00:00

Good morning, I am having an issue after pushing my application to IIS. While developing, using IIS Express, any time I selected Logout from the Application it would Logout and if I selected Login it would bring me to the Login screen. In IIS served application, when I hit Logout it logs out of the application. When I select Login, it automatically logs me into the application. I have looked over everything code wise and cannot see where it went wrong, though it could also be a IIS setting that I am not seeing.

The setup is that the API is using OpenIddict, and the client is passing a logout to the API and on a "successful" logout from the API, it sends back a true so that the client can logout.
Here is my API startup:

public class Startup  
{  
    public Startup(IConfiguration configuration, IWebHostEnvironment env)  
    {  
        Configuration = configuration;  
        _env = env;  
    }  
  
    public IConfiguration Configuration { get; }  
    private readonly IWebHostEnvironment _env;  
    // This method gets called by the runtime. Use this method to add services to the container.  
    public void ConfigureServices(IServiceCollection services)  
    {  
        services.AddCors(options =>  
        {  
            options.AddPolicy("MRM2IncPolicy", builder =>  
            {  
                builder.WithOrigins(Configuration.GetSection("Cors:Origins").GetChildren().Select(c => c.Value).ToArray())  
                .AllowAnyHeader()  
                .AllowAnyMethod()  
                .AllowCredentials();  
            });  
        });  
  
        services.AddControllers();  
        services.AddRazorPages();  
  
        services.AddDbContext<IdentDbContext>(options =>  
        {  
            options.UseSqlServer(  
                Configuration.GetConnectionString("IdentityDB"));  
  
            options.UseOpenIddict();  
        });  
          
        // Add the Identity Services we are going to be using the Application Users and the Application Roles  
        services.AddIdentity<ApplicationUsers, ApplicationRoles>()  
            .AddEntityFrameworkStores<IdentDbContext>()  
            .AddUserStore<ApplicationUserStore>()  
            .AddRoleStore<ApplicationRoleStore>()  
            .AddRoleManager<ApplicationRoleManager>()  
            .AddUserManager<ApplicationUserManager>()  
            .AddErrorDescriber<ApplicationIdentityErrorDescriber>()  
            .AddDefaultTokenProviders()  
            .AddDefaultUI();  
  
        services.Configure<IdentityOptions>(options =>  
        {  
            // Configure Identity to use the same JWT claims as OpenIddict instead  
            // of the legacy WS-Federation claims it uses by default (ClaimTypes),  
            // which saves you from doing the mapping in your authorization controller.  
            options.ClaimsIdentity.UserNameClaimType = Claims.Name;  
            options.ClaimsIdentity.UserIdClaimType = Claims.Subject;  
            options.ClaimsIdentity.RoleClaimType = Claims.Role;  
  
            // Configure the options for the Identity Account  
            options.SignIn.RequireConfirmedEmail = true;  
            options.SignIn.RequireConfirmedAccount = true;  
            options.User.RequireUniqueEmail = true;  
            options.Lockout.MaxFailedAccessAttempts = 3;  
            options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(10);  
        });  
  
  
        // OpenIddict offers native integration with Quartz.NET to perform scheduled tasks  
        // (like pruning orphaned authorizations/tokens from the database) at regular intervals.  
        services.AddQuartz(options =>  
        {  
            options.UseMicrosoftDependencyInjectionJobFactory();  
            options.UseSimpleTypeLoader();  
            options.UseInMemoryStore();  
        });  
  
        // Register the Quartz.NET service and configure it to block shutdown until jobs are complete.  
        services.AddQuartzHostedService(options => options.WaitForJobsToComplete = true);  
  
        services.AddOpenIddict()  
            // Register the OpenIddict core components.  
            .AddCore(options =>  
            {  
                // Configure OpenIddict to use the Entity Framework Core stores and models.  
                // Note: call ReplaceDefaultEntities() to replace the default entities.  
                options.UseEntityFrameworkCore()  
                .UseDbContext<IdentDbContext>();  
  
                options.UseQuartz();  
            })  
            // Register the OpenIddict server components.  
            .AddServer(options =>  
            {  
                // Enable the token endpoint.  What other endpoints?  
                options.SetAuthorizationEndpointUris("/api/Authorization/Authorize")  
                .SetTokenEndpointUris("/Token")  
                .SetLogoutEndpointUris("/api/Logout/LogoutPostAnsync")  
                .SetIntrospectionEndpointUris("/Introspect")  
                .SetUserinfoEndpointUris("/api/Userinfo/Userinfo")  
                .SetVerificationEndpointUris("/Verify");  
  
                // Mark the "OpenId", "email", "profile" and "roles" scopes as supported scopes.  
                options.RegisterScopes(Scopes.OpenId, Scopes.Email, Scopes.Profile, Scopes.Roles);  
  
                // Enable the available flows.  Which flow do I need?  
                options.AllowClientCredentialsFlow()  
                .AllowImplicitFlow()  
                .AllowAuthorizationCodeFlow()  
                .RequireProofKeyForCodeExchange()  
                .AllowRefreshTokenFlow();  
  
                if (_env.IsDevelopment())  
                {  
                    // Register the signing and encryption credentials.  
                    options.AddDevelopmentEncryptionCertificate()  
                          .AddDevelopmentSigningCertificate();  
                }  
                else if (_env.IsProduction() || _env.IsStaging())  
                {  
                    // need a signing certificate and encryption certificate thumbprint.  
                    options.AddSigningCertificate(Configuration.GetSection("CertifcateThumbprints:SigningCertificate").Value)  
                    .AddEncryptionCertificate(Configuration.GetSection("CertifcateThumbprints:EncryptionCertificate").Value);  
                }  
  
                // Register the ASP.NET Core host and configure the ASP.NET Core options.  
                options.UseAspNetCore()  
                       .EnableAuthorizationEndpointPassthrough()  
                       .EnableLogoutEndpointPassthrough()  
                       .EnableTokenEndpointPassthrough()  
                       .EnableStatusCodePagesIntegration()  
                       .EnableUserinfoEndpointPassthrough()  
                       .EnableVerificationEndpointPassthrough();  
            })  
            // Register the OpenIddict validation components.  
            .AddValidation(options =>  
            {  
                // Import the configuration from the local OpenIddict server instance.  
                options.UseLocalServer();  
  
                options.UseSystemNetHttp();  
                  
                // Register the ASP.NET Core host.  
                options.UseAspNetCore();  
            });  
  
        // Register the Swagger generator, defining 1 or more Swagger documents  
        services.AddSwaggerGen(swagger =>   
        {  
            swagger.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme()  
            {  
                Name = "Authorization",  
                Type = SecuritySchemeType.Http,  
                Scheme = "Bearer",  
                BearerFormat = "JWT",  
                In = ParameterLocation.Header,  
                Description = "JWT Authorization header using the Bearer scheme. \r\n\r\n Enter 'Bearer'[space] and then your token in the text input below.\r\n\r\nExample: \"Bearer 12345abcdef\""  
            });  
            swagger.AddSecurityRequirement(new OpenApiSecurityRequirement  
            {  
                {  
                    new OpenApiSecurityScheme  
                    {  
                        Reference = new OpenApiReference  
                        {  
                            Type = ReferenceType.SecurityScheme,  
                            Id = "Bearer"  
                        }  
                    },  
                    Array.Empty<string>()  
                }  
            });  
            swagger.OperationFilter<SwaggerDefaultValues>();  
            swagger.OperationFilter<AuthenticationRequirementOperationFilter>();  
            swagger.ResolveConflictingActions(apiDescriptions => apiDescriptions.First());  
  
            // Set the comments path for the Swagger JSON and UI.  
            var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";  
            var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);  
            swagger.IncludeXmlComments(xmlPath);  
        });  
        services.AddApiVersioning();  
        services.AddVersionedApiExplorer(options =>  
        {  
            options.GroupNameFormat = "'v'VVVV";  
            options.DefaultApiVersion = ApiVersion.Parse("0.10.alpha");  
            options.AssumeDefaultVersionWhenUnspecified = true;  
        });  
  
  
        services.AddDataLibrary();  
  
        // Add in the email  
        var emailConfig = Configuration.GetSection("EmailConfiguration").Get<EmailConfiguration>();  
        services.AddSingleton(emailConfig);  
        services.AddEmailLibrary();  
        services.AddTransient<IConfigureOptions<SwaggerGenOptions>, ConfigureSwaggerOptions>();  
        if (_env.IsDevelopment())  
        {  
            services.AddHostedService<TestData>();  
        }  
        else if (_env.IsStaging() || _env.IsProduction())              
        {  
            services.AddHostedService<ProdStageSeed>();  
        }  
    }  
  
    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.  
    public void Configure(IApplicationBuilder app, IApiVersionDescriptionProvider provider)  
    {  
        if (_env.IsDevelopment())  
        {  
            app.UseDeveloperExceptionPage();  
        }  
        else  
        {  
            app.UseStatusCodePagesWithReExecute("/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.UseCors("MRM2IncPolicy");  
  
        app.UseHttpsRedirection();  
        app.UseStaticFiles();  
  
        // Enable middleware to serve generated Swagger as a JSON endpoint.  
        app.UseSwagger();  
  
        // Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.),  
        // specifying the Swagger JSON endpoint.  
        app.UseSwaggerUI(c =>  
        {                 
            c.DisplayOperationId();  
            var versionDescription = provider.ApiVersionDescriptions;  
            foreach (var description in provider.ApiVersionDescriptions.OrderByDescending(_ => _.ApiVersion))  
            {  
                c.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json", $"MRM2 Identity API {description.GroupName}");  
            }  
        });  
  
        app.UseRouting();  
  
        app.UseAuthentication();  
  
        app.UseAuthorization();  
  
        app.UseEndpoints(endpoints =>  
        {  
            endpoints.MapRazorPages();  
            endpoints.MapControllers();  
        });  
    }  
}  

Logout Controller in the API:

public class LogoutController : ControllerBase  
{  
    private readonly SignInManager<ApplicationUsers> _signInManager;  
    private readonly ILogger<LogoutController> _logger;  
  
    public LogoutController(SignInManager<ApplicationUsers> signInManager, ILogger<LogoutController> logger)  
    {  
        _signInManager = signInManager;  
        _logger = logger;  
    }  
  
    /// <summary>  
    /// Signs a users out of the application and returns a true or false  
    /// </summary>  
    /// <returns></returns>  
    [HttpPost(Name = nameof(LogoutPostAsync))]  
    public async Task<bool> LogoutPostAsync()  
    {  
        bool output = false;  
        var task = _signInManager.SignOutAsync();  
  
        if (task.IsCompletedSuccessfully)  
        {  
            output = true;  
            await _signInManager.SignOutAsync();  
            await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);  
            await HttpContext.SignOutAsync(IdentityConstants.ApplicationScheme);  
            SignOut(  
                authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,  
                properties: new AuthenticationProperties  
                {  
                    RedirectUri = "/"  
                });  
        }  
        return output;  
    }  
}  

Client Startup:

public class Startup  
    {  
        public Startup(IConfiguration configuration)  
        {  
            Configuration = configuration;  
        }  
  
        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)  
        {  
  
            services.AddAuthentication(options =>  
            {  
                options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;  
            })  
                .AddCookie(option =>  
                {  
                    option.LoginPath = "/Identity/Account/Login";  
                    option.AccessDeniedPath = "/Identity/Account/AccessDenied";  
                    option.ExpireTimeSpan = TimeSpan.FromMinutes(60);  
                    option.SlidingExpiration = false;  
  
                    option.Cookie.Name = "IdentityDbCookie";   
                    option.Cookie.SameSite = SameSiteMode.None;  
                })  
                .AddOpenIdConnect(options =>  
                {  
                    options.ClientId = Configuration.GetSection("openId:ClientId").Value;  
                    options.ClientSecret = Configuration.GetSection("openId:ClientSecret").Value;  
  
                    options.RequireHttpsMetadata = true;  
                    options.GetClaimsFromUserInfoEndpoint = true;  
                    options.SaveTokens = true;  
  
                    options.ResponseType = OpenIdConnectResponseType.Code;  
                    options.AuthenticationMethod = OpenIdConnectRedirectBehavior.RedirectGet;  
  
                    options.Authority = new Uri(Configuration.GetSection("BaseAddresses:Api").Value).ToString();                      
  
                    // This will change with .net 6.0  
                    options.SecurityTokenValidator = new JwtSecurityTokenHandler  
                    {  
                        InboundClaimTypeMap = new Dictionary<string, string>()  
                    };  
  
                    options.TokenValidationParameters.NameClaimType = "name";  
                    options.TokenValidationParameters.RoleClaimType = "role";  
                });  
  
            string basePath = Configuration.GetSection("BaseAddresses:Api").Value;  
            services.AddUiApiClient(basePath);  
            services.AddHttpContextAccessor();  
  
            services.AddAuthorization(options =>  
            {  
                options.AddPolicy("LotroAdmin", policy => policy.RequireRole("LOTRO Administrator"));  
                options.AddPolicy("SiteAdmin", policy => policy.RequireRole("Site Administrator"));  
                options.AddPolicy("OpenIDAdmin", policy => policy.RequireRole("OpenID Administrator"));  
            });  
  
            services.AddRazorPages(options =>  
            {  
                options.Conventions.AuthorizeAreaFolder("Identity", "/Manage");  
                options.Conventions.AuthorizeAreaFolder("Administration", "/Lotro", "LotroAdmin");  
                options.Conventions.AuthorizeAreaFolder("Administration", "/UsersRoles", "SiteAdmin");  
                options.Conventions.AuthorizeAreaFolder("Administration", "/OpenIDManagement", "OpenIDAdmin");  
            });  
  
            services.AddHttpClient();  
        }  
  
        // 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("/Error");  
                app.UseHsts();  
            }  
  
            app.UseHttpsRedirection();  
            app.UseStaticFiles();  
  
            app.UseRouting();  
  
            app.UseAuthentication();  
            app.UseAuthorization();  
  
            app.UseEndpoints(endpoints =>  
            {  
                endpoints.MapRazorPages();  
            });  
        }  
    }  

Logout in the client:

public async Task<IActionResult> OnPost(string returnUrl = null)  
    {  
        var token = await HttpContext.GetTokenAsync(SessionKeyName);  
        _client.SetBearerToken(token);  
        var task = await _client.LogoutPostAsync(apiVersion);  
        if (task == true)  
        {  
            await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);   
            Response.Cookies.Delete(".AspNetCore.Identity.Application");  
  
            _logger.LogInformation("successfully logged out");  
            return RedirectToPage("/Index");  
        }  
        else  
        {  
            return RedirectToPage("/Error");  
        }  
    }  

If I watch the Application in the browser I can see that all the cookies are removed. Not sure if IIS is holding something that I cannot see. Any thoughts?

Internet Information Services
ASP.NET Core
ASP.NET Core
A set of technologies in the .NET Framework for building web applications and XML web services.
4,596 questions
{count} votes

Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.