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?