I have a WebApi project that has endpoints for all the business logic, including an implementation of the Identity endpoints for login/register/etc.
The cookie authentication works fine on the WebApi project as in I can log in, then call other endpoints that require authorization.
This is the Program.cs for the Razor project:
var builder = WebApplication.CreateBuilder(args);
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddRazorPages();
builder.Services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo(builder.Environment.ContentRootPath + "/keys"))
.SetApplicationName("MarketManager");
builder.Services
.AddDbContext<ApplicationDbContext>(
options => options
.UseSqlServer(connectionString));
builder.Services
.AddDefaultIdentity<Person>()
.AddRoles<AppRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.ConfigureApplicationCookie(options =>
{
options.Cookie.Name = ".MarketManager.SharedCookie";
options.Cookie.Path = "/";
options.LoginPath = "/Login";
options.LogoutPath = "/Logout";
});
var app = builder. Build();
app.Use(async (context, next) =>
{
await next();
if (context.Response.StatusCode == 404)
{
context.Request.Path = "/not-found";
await next();
}
});
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
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.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapRazorPages();
app.Run();
And the API project's Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddDomainServices(builder.Configuration);
builder.Services.AddApplicationServices();
builder.Services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo(builder.Environment.ContentRootPath + "/keys"))
.SetApplicationName("MarketManager");
builder.Services
.AddDefaultIdentity<Person>()
.AddRoles<AppRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.ConfigureApplicationCookie(options =>
{
options.Cookie.Name = ".MarketManager.SharedCookie";
options.Cookie.Path = "/";
options.LoginPath = "/Login";
options.LogoutPath = "/Logout";
});
builder.Services.AddAuthorization();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.MapIdentityApi<Person>();
app.MapGroup("/roles")
.MapRoles();
await app.RunAsync();
The key I've copied from one project and pasted to the other so they have the same value (when I push this to production they'll be in different subdomains / hosting spaces).
The Person class inherits from IdentityUser.
My plan is to have the Razor pages call the API for all it's operations and to not implement any logic in the razor page .cs files.
If I use swagger to access the API directly, it does what it's supposed to. The razor page _loginpartial.cshtml will even pick up the cookie value and display the user name.
If I try to use the razor pages to call the login endpoint, it steps through all the right code, returns a success result, but it doesn't generate the cookie in the browser and it redirects straight back to the login page.
This is the login page code:
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
returnUrl ??= Url.Content("~/");
if (ModelState.IsValid)
{
using (var client = new HttpClient())
{
var response = await client.PostAsJsonAsync("http://localhost:5122/identity/login?useCookies=true", Input);
switch (response.StatusCode)
{
case System.Net.HttpStatusCode.OK:
return Redirect(returnUrl);
case System.Net.HttpStatusCode.BadRequest:
var errorText = response.Content.ReadAsStringAsync().Result;
ModelState.AddModelError(string.Empty, errorText);
return Page();
}
}
}
// Incorrect input
return Page();
}
Firstly,it was httpclient sent request instead of your browser,you have to read cookie from response and set it to browser,modify codes in your page handler to:
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
returnUrl ??= Url.Content("~/");
if (ModelState.IsValid)
{
CookieContainer cookies = new CookieContainer();
HttpClientHandler handler = new HttpClientHandler();
handler.CookieContainer = cookies;
var client = new HttpClient(handler);
var response = await client.PostAsJsonAsync(" https://localhost:7096/login?useCookies=true", Input);
switch (response.StatusCode)
{
case System.Net.HttpStatusCode.OK:
var respCookies = cookies.GetCookies(new Uri("https://localhost:7096"));
var authCookie = respCookies.Where(x => x.Name == ".MarketManager.SharedCookie").FirstOrDefault();
if (authCookie != null)
{
HttpContext.Response.Cookies.Delete(".MarketManager.SharedCookie");
HttpContext.Response.Cookies.Append(".MarketManager.SharedCookie", authCookie.Value, new CookieOptions()
{
Domain = authCookie.Domain,
HttpOnly = authCookie.HttpOnly,
SameSite = SameSiteMode.None,
Path = authCookie.Path,
Secure = authCookie.Secure,
Expires = authCookie.Expires,
});
}
return Redirect(returnUrl);
case System.Net.HttpStatusCode.BadRequest:
var errorText = response.Content.ReadAsStringAsync().Result;
ModelState.AddModelError(string.Empty, errorText);
return Page();
}
}
// If we got this far, something failed, redisplay form
return Page();
}
Secondly,you should set the key file to same position in two projects,the content root path for different projects are different ,for more details,see this document
builder.Services.AddDataProtection()
.PersistKeysToFileSystem({PATH TO COMMON KEY RING FOLDER BY DEFAULT})
.SetApplicationName("MarketManager");
I tried a minimal example on myside,and add [Authorize]
attribute to Privacy page:
If you meet subdomain related issue,follow this part of document