ASP.NET Core 3.1 HttpContext.SignOutAsync does not Redirect - asp.net-core

When I use HttpContext.SignOutAsync with AuthenticationProperties together with a RedirectUri I expect to be redirected to a URL, but instead I am not redirected.
How can I debug this? I do not see any Exception or Warning.
Is this my implementation swallowing this?
HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme,
new AuthenticationProperties { RedirectUri = "/" });
Here is the Logout Implementation:
[AllowAnonymous]
public async Task Logout()
{
var oidcAuth = false;
// clear the auth cookies
if (HttpContext.Request.Cookies.Count> 0)
{
foreach (var (key, _) in HttpContext.Request.Cookies)
{
if (key.Contains(Startup.COOKIE_NAME_BASIC))
{
Response.Cookies.Delete(key);
} else if (key.Contains(Startup.COOKIE_NAME_OIDC))
{
oidcAuth = true;
Response.Cookies.Delete(key);
}
}
}
HttpContext.Session.Clear();
if (oidcAuth)
{
await HttpContext.SignOutAsync(Startup.COOKIE_NAME_OIDC);
await HttpContext.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme);
}
else
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme,
new AuthenticationProperties { RedirectUri = "/" });
}
}

Firstly,you can refer to the official doc,and you can see RedirectUri is only used on a few specific paths by default, for example, the login path and logout paths.
So if you want to redirect,you need to make sure your current path is login or logout path.Here is a demo:
Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.LoginPath = "/Home/Login";
options.LogoutPath = "/Home/Logout";
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/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.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseSession();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
HomeController:
[HttpPost]
public async Task Logout()
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme,new AuthenticationProperties { RedirectUri="/"});
}
result:

The official documentation does not mention what the "special" conditions are, but the source code does. No Exception or warning is given when the Redirect URI is being ignored.
The source code located here explains it all:
// Only redirect on the login path
var shouldRedirect = Options.LoginPath.HasValue && OriginalPath == Options.LoginPath;
await ApplyHeaders(shouldRedirect, signedInContext.Properties);
Logger.AuthenticationSchemeSignedIn(Scheme.Name);
Some important bits:
In Startup make sure the options.LogoutPath matches the path of
your actual LogoutController's Logout Action.
Secondly the Logout Action should not perform the redirect and
should return a Task and not a Task<IActionResult>
Also not to handle the OnRedirectToReturnUrl yourself.
LogoutController: Sign-out and set the Redirect URI The Path = "Logout/Logout"
[AllowAnonymous]
[HttpPost]
public async Task Logout()
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme,
new AuthenticationProperties { RedirectUri = "/" });
}
Startup: Configure the path
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme,"Cookie",options => {
// must match path of Logout Controller
options.LogoutPath = new PathString("/Logout/Logout");
// do not handle the event yourself
// options.Events.OnRedirectToReturnUrl = async context =>
// {
// await Task.CompletedTask;
// };

Related

ASP .NET Core CORS issue with Google authentication on redirect

Been following this tutorial in order to implement Google authentication in my web API but on the client side (using React and axios to do the request) the authentication process gets interrupted with this CORS issue and I'm struggling to sort it out:
Access to XMLHttpRequest at 'https://accounts.google.com/o/oauth2/v2/auth?(etc)' (redirected from 'https://localhost:44320/Photo/b997d788-3812-41d0-a09d-1a597eee9bad') from origin 'https://localhost:8080' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
This is the Startup.cs file:
namespace rvc
{
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.AddCors(options =>
{
options.AddDefaultPolicy(builder =>
{
builder.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod();
});
});
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
}).AddCookie(options =>
{
options.LoginPath = "/account/google-login";
}).AddGoogle(options =>
{
options.ClientId = "clientId";
options.ClientSecret = "secret";
});
services.AddScoped<PhotoService>();
services.AddScoped<TagService>();
services.AddScoped(_ => new BlobServiceClient(Configuration.GetConnectionString("AzureBlobStorage")));
services.AddDbContext<Data.DataContext>(x => x.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddControllers().AddJsonOptions(options =>
{
options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
});
services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "rvc", Version = "v1" }); });
}
// 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();
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "rvc v1"));
}
app.UseHttpsRedirection();
if (env.IsProduction())
{
app.UseSpa(spa => { });
app.UseFileServer(new FileServerOptions
{
FileProvider = new PhysicalFileProvider(
Path.Combine(env.ContentRootPath, "client")),
EnableDefaultFiles = true
});
}
app.UseRouting();
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
}
}
}
The Route("google-login") gets called but the Url.Action("GoogleResponse") is not reached. These are the Google Authentication methods:
namespace rvc.Controllers;
[AllowAnonymous, Route("account")]
public class AccountController : Controller
{
[Route("google-login")]
public IActionResult GoogleLogin()
{
var properties = new AuthenticationProperties {RedirectUri = Url.Action("GoogleResponse")};
return Challenge(properties, GoogleDefaults.AuthenticationScheme);
}
[Route("google-response")]
public async Task<IActionResult> GoogleResponse()
{
var result = await HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);
var claims = result.Principal?.Identities.FirstOrDefault()
?.Claims.Select(claim => new
{
claim.Issuer,
claim.OriginalIssuer,
claim.Type,
claim.Value
});
return Json(claims);
}
}
This is probably because from the server you use redirect, which triggers CORS (even if from your server you allow it).
you have to return the redirect URL to your front-end in some other way, capture it from the front-end app and then call the URL you need to invoke.

PasswordSignIn return Succeed but after redirect to another ActionResult User is not authenticated in net core 3.1

I user net core 3.1 and EF core to identity and login.
At first , I use passwordSignIn method to signin and returns Succeed after that I retdirectToAction to "profile".
in "Profile" User.Identity.isAuthenticated is false.
As you can see in my code I set sign in complete and works correct. but user is not authenticeted.
here is my sign in:
[HttpPost]
public async Task<IActionResult> SignUp(string username, string password)
{
var user = _db.Users.Where(p => p.UserName == username).FirstOrDefault();
if (user != null)
{
var res = await _signInManager.PasswordSignInAsync(user, password, true, false);
if (res.Succeeded)
{
return RedirectToAction("profile");
}
}
return View();
}
and here is profile:
public IActionResult Profile()
{
var t = User.Identity.IsAuthenticated;
var n = User.Identity.Name;
var s = User.Claims.ToList();
var x = _userManager.GetUserId(User);
var ss = User.IsInRole("Admin");
return View();
}
and here is my start up :
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.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddControllersWithViews();
services.AddDbContext<MyContext>(opt =>
{
opt.UseSqlServer(Encryptor.Decrypt(Configuration.GetConnectionString("DefaultConnection")));
});
var builder = services.AddIdentityCore<User>();
var identityBuilder = new IdentityBuilder(builder.UserType, builder.Services);
identityBuilder.AddRoles<UserRole>();
identityBuilder.AddEntityFrameworkStores<MyContext>();
identityBuilder.AddSignInManager<SignInManager<User>>();
services.ConfigureApplicationCookie(options =>
{
options.Cookie.HttpOnly = true;
options.ExpireTimeSpan = TimeSpan.FromMinutes(30);
options.LoginPath = "/Account/Login";
options.AccessDeniedPath = "/Account/AccessDenied";
options.SlidingExpiration = true;
});
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie("Identity.Application");
}
// 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("/Home/Error");
}
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "Admin",
pattern: "{area:exists}/{controller=Admin}/{action=Index}/{id?}");
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
Update 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.AddControllersWithViews();
services.AddDbContext<MyContext>(opt =>
{
opt.UseSqlServer(Encryptor.Decrypt(Configuration.GetConnectionString("DefaultConnection")));
});
var builder = services.AddIdentityCore<AppUser>();
var identityBuilder = new IdentityBuilder(builder.UserType, builder.Services);
identityBuilder.AddRoles<Role>();
identityBuilder.AddEntityFrameworkStores<MyContext>().AddDefaultTokenProviders();
identityBuilder.AddSignInManager<SignInManager<AppUser>>();
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.ConsentCookie.IsEssential = true;
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.Configure<IdentityOptions>(options =>
{
options.SignIn.RequireConfirmedEmail = false;
options.SignIn.RequireConfirmedAccount = false;
options.SignIn.RequireConfirmedPhoneNumber = false;
});
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie("Identity.Application");
services.AddMvc();
}
// 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("/Home/Error");
}
app.UseStaticFiles();
app.UseRouting();
app.UseCookiePolicy();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "Admin",
pattern: "{area:exists}/{controller=Admin}/{action=Index}/{id?}");
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
Finally I solve problem with the help of #Yinqiu and a bit more search.
I add these lines to signin method:
var claims = new[]
{
new Claim("name", authUser.Username)
};
var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(identity));
I dont have any idea about how it solve but it works.
of course I change line in start up class :
... .AddCookie("Cookie");

AspNetCore 2.2 as API - How to logout using Identity methods?

I created web application with Angular 7 as front-end,
DotNetCore 2.2 as back-end API (SQL Server for db).
When I initially created the project, I didn't add Authentication, because
there is no need of UI pages. I plan to use Identity authentication with cookies.
While Login and [Authorize] work well, Logout doesn't work with signInManager.SignOutAsync().
Microsoft states:
"SignOutAsync clears the user's claims stored in a cookie.
Don't redirect after calling SignOutAsync or the user will not be signed out."
Seems that the cookie is not deleted and it is accepted as valid from API (except when it is expired).
I tried with Response.Cookies.Delete() and HttpContext.SignOutAsync() without success.
public void ConfigureServices(IServiceCollection services)
{
services.AddCors(options =>
{
options.AddPolicy("CORS", builder =>
{
builder.WithOrigins("http://localhost:42000")
.AllowAnyHeader()
.AllowCredentials();
});
});
services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => false;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddDbContext<NGDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddIdentity<NGUser, IdentityRole>()
.AddEntityFrameworkStores<NGDbContext>()
.AddDefaultTokenProviders();
services.Configure<IdentityOptions>(options =>
{
// some options...
});
services.ConfigureApplicationCookie(options =>
{
options.Cookie.HttpOnly = true;
options.ExpireTimeSpan = TimeSpan.FromMinutes(1);
options.SlidingExpiration = true;
//options.CookieName = "MyCookie";
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddScoped<NamesService, NamesService>();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
InitialSeeder.Seed(app);
}
else
{
app.UseHsts();
}
app.UseCors("CORS");
app.UseHttpsRedirection();
app.UseCookiePolicy();
app.UseAuthentication();
app.UseMvc();
}
[Route("api/[controller]")]
[ApiController]
public class AuthController : ControllerBase
{
private UserManager<NGUser> userManager;
private SignInManager<NGUser> signInManager;
public AuthController(UserManager<NGUser> userManager, SignInManager<NGUser> signInManager)
{
this.userManager = userManager;
this.signInManager = signInManager;
}
[HttpPost("Login")]
public async Task<ActionResult> Login(LoginModel model)
{
var user = await this.userManager.FindByNameAsync(model.Username);
if (user == null)
{
return Unauthorized();
}
var signInResult = await this.signInManager.PasswordSignInAsync(user, model.Password, false, false);
if (!signInResult.Succeeded)
{
return Unauthorized();
}
return Ok();
}
[HttpPost("Logout")]
public async Task<ActionResult> Logout()
{
await this.signInManager.SignOutAsync();
return Ok();
}
}
Is it possible to benefit from Identity sign-in sign-out methods?
Or should use custom methods?
For successful logout signInManager.SignOutAsync() just needs a cookie.
Which I didn't supply with Angular HttpClient.post() method - I missed null as second argument.
Working Angular Side:
login(model: LoginModel){
return this.http.post("https://localhost:50000/api/auth/login", model, { withCredentials: true });
}
logout(){
return this.http.post("https://localhost:50000/api/auth/logout", null, { withCredentials: true });
}
Working .Net Side:
Both logout approaches work:
this.HttpContext.Response.Cookies.Delete(".AspNetCore.Identity.Application");
await this.signInManager.SignOutAsync();
The first deletes only selected cookie, while the second deletes three .AspNetCore.Identity.Application, Identity.External ,Identity.TwoFactorUserId.
SignOutAsync calls three methods:
await Context.SignOutAsync(IdentityConstants.ApplicationScheme);
await Context.SignOutAsync(IdentityConstants.ExternalScheme);
await Context.SignOutAsync(IdentityConstants.TwoFactorUserIdScheme);
Yes, it is possible to use Identity in API.

ASP Core 3 react template, HttpContext.User.IsAuthenticated() returns False after login

After working on my project for a while, I released the HttpContext.User.IsAuthenticated() returns False after login and I need to know where I should look for the mistake I made that cause this problem.
This is the Login, OnPost method.
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
returnUrl = returnUrl ?? Url.Content("~/");
if (ModelState.IsValid)
{
var user = _userManager.Users.FirstOrDefault(u => u.StudentNumber == Input.StudentNumber.ToString());
if (!(user is null) && await _userManager.CheckPasswordAsync(user, Input.Password))
await _signInManager.SignInAsync(user, Input.RememberMe);
var isUserAuthenticated = HttpContext.User.IsAuthenticated();
return Redirect(returnUrl);
}
// If we got this far, something failed, redisplay form
return Page();
}
The ConfigureServices method.
public void ConfigureServices(IServiceCollection services)
{
services.AddAutoMapper();
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDefaultIdentity<ApplicationUser>(option=>option.Password.RequireNonAlphanumeric=false)
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>();
services.AddAuthentication()
.AddIdentityServerJwt();
services.AddMvc(options => options.EnableEndpointRouting = false)
.AddNewtonsoftJson();
// In production, the React files will be served from this directory
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "ClientApp/build";
});
}
The Configure method.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/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.UseHttpsRedirection();
app.UseStaticFiles();
app.UseSpaStaticFiles();
app.UseAuthentication();
app.UseIdentityServer();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller}/{action=Index}/{id?}");
});
app.UseSpa(spa =>
{
spa.Options.SourcePath = "ClientApp";
if (env.IsDevelopment())
{
spa.UseReactDevelopmentServer(npmScript: "start");
}
});
}
SignInManager.SignInAsync() only creates the cookie for the given user. This method would not set HttpContext.User.
But in the next request which has the cookie you can access HttpContext.User after AuthenticationMiddleware and HttpContext.User.IsAuthenticated() should be true.
AuthenticationMiddleware always try to authenticate user with the default scheme and since you have AddIdentityServer after AddDefaultIdentity, identity server is becoming your default scheme, but when you call SignInManager.SignInAsync the Identity scheme is triggered.
To sum up, with this configuration your AuthenticationMiddleware always tries to authenticate request for IdentityServer and if you want other scheme for you apis you should use [Authorize(AuthenticationSchemes = "Identity.Application")].
P.S. Identity.Application is authenticatio scheme for ASP.NET Identity

User is not authorized after successful PasswordSignInAsync

I use Postman to send request to /api/account/login and user logs in, then I send request to /api/account/logout and it logs out successfully, but when I do this in frontend, I fill in the log in form and send the request, I get response.ok, but when I try to send request to /api/account/logout it throws 401 Unauthorized.
I think there is a problem in my Startup.cs ConfigureServices method, but I'm not sure what it is
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().AddJsonOptions(x =>
x.SerializerSettings.ReferenceLoopHandling =
Newtonsoft.Json.ReferenceLoopHandling.Ignore);
services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("RemoteConnection")));
services.AddIdentity<User, Role>()
.AddEntityFrameworkStores<AppDbContext, int>()
.AddDefaultTokenProviders();
services.Configure<IdentityOptions>(options =>
{
options.Cookies.ApplicationCookie.AutomaticChallenge = false;
});
// services.AddScoped<UserGroupRepository, UserGroupRepository>();
services.AddScoped<IUnitOfWork, UnitOfWork>();
services.AddScoped<ISurveysRepository, SurveysRepository>();
services.AddScoped<IUsersRepository, UsersRepository>();
} // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions
{
HotModuleReplacement = true,
ReactHotModuleReplacement = true
});
}
app.UseStaticFiles();
app.UseIdentity();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
routes.MapSpaFallbackRoute(
name: "spa-fallback",
defaults: new { controller = "Home", action = "Index" });
});
}
I noticed, when sending request using form that even here the user is not authorized
[HttpPost("login")]
public async Task<IActionResult> Login([FromBody] LoginRequest request)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var result = await _signInManager.PasswordSignInAsync(request.UserName, request.Password, true, false);
if (!result.Succeeded)
{
return StatusCode((int)HttpStatusCode.Conflict, new ErrorResponse
{
ErrorMessage = "Invalid User Name or Password."
});
}
if (User.Identity.IsAuthenticated)
return NoContent();
else
return BadRequest(ModelState);
}
Was a mistake in my api.js performRequest method, I missed
credentials: 'include'