question

MichaelMastroII-1060 avatar image
0 Votes"
MichaelMastroII-1060 asked MichaelMastroII-1060 commented

Logout does not seem to be working correctly

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?

windows-server-iisdotnet-aspnet-core-razordotnet-aspnet-core-securitydotnet-aspnet-core-auth
· 8
5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

Hi @MichaelMastroII-1060 ,

According to your description, this situation generally occurs because of cookies. I suggest that you can provide the relevant configuration in the web.config file.

0 Votes 0 ·

The scenario sounds like you've logged out of the application but not the authentication server. Therefore when you login the authentication server sees its cookie and redirects you back to the web application.

0 Votes 0 ·

@AgaveJoe That makes sense, but I am calling the _signInManager.SignOutAsync() in the Logout controller. So it should technically be signing out of the application?

@YurongDai-MSFT Here is the what is in the Web.Config on the IIS server:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<location path="." inheritInChildApplications="false">
<system.webServer>
<handlers>
<add name="aspNetCore" path="" verb="" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
</handlers>
<aspNetCore processPath="dotnet" arguments=".\IdentityApi.UI.RazorPages.dll" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" hostingModel="inprocess" />
</system.webServer>
</location>
<system.web>
<httpCookies httpOnlyCookies="true" requireSSL="true" sameSite="Lax" />
</system.web>
<system.webServer>
<security>
<requestFiltering>
<verbs>
<add verb="TRACE" allowed="false" />
<add verb="TRACK" allowed="false" />
</verbs>
</requestFiltering>
</security>
</system.webServer>
</configuration>



0 Votes 0 ·
AgaveJoe avatar image AgaveJoe MichaelMastroII-1060 ·

That makes sense, but I am calling the _signInManager.SignOutAsync() in the Logout controller. So it should technically be signing out of the application?

Not necessarily if you are using a authentication server. The _signInManager.SignOutAsync() logs out of the web application. Usually there is a command that sends a message to the authentication server to logout there as well; federated logout.

Anyway, that's how it works on my end using OAuth/OIDC.



0 Votes 0 ·
Show more comments

0 Answers