Blazor WASM Authentication along with Blazor Server Authentication - authentication

When using ApiAuthorizationDbContext WASM Fetch page authenticates and passes the token, but Razor pages will not authenticate. When switching to IdentityDbContext, the opposite happens, I am able to authenticate the razor page but WASM fetch page will not authenticate. I have a very simple sample at https://github.com/williameduardo79/BlazorServerClientSample
This works well with Blazor WASM
public class ApplicationDbContext : ApiAuthorizationDbContext<ApplicationUser>
{
public ApplicationDbContext(
DbContextOptions options,
IOptions<OperationalStoreOptions> operationalStoreOptions) : base(options, operationalStoreOptions)
{
}
}
This works well with Blazor Pages
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
}
How can I make it work?
Any references are appreciated :)

Can you share your server's Startup.cs file? Specifically where you are registering/configuring the services for Identity/DbContexts. IdentityDbContext is basically the same as ApiAuthorizationDbContext but it adds the required DbSets for Persisted Grants and Device Flow Codes for enabling API-based authentication.
I have a working IdentityDbContext for both MVC Razor pages and Blazor WASM so I know it is possible! :) The difference being I implemented the types explicitly:
public class ApplicationDbContext : IdentityDbContext<ApplicationUser, ApplicationRole, string,
IdentityUserClaim<string>, ApplicationUserRole, IdentityUserLogin<string>, IdentityRoleClaim<string>,
IdentityUserToken<string>>, IPersistedGrantDbContext
{
private readonly IOptions<OperationalStoreOptions> _operationalStoreOptions;
public ApplicationDbContext(DbContextOptions options, IOptions<OperationalStoreOptions> operationalStoreOptions) : base(options)
{
_operationalStoreOptions = operationalStoreOptions;
}
public DbSet<ApplicationUser> ApplicationUser { get; set; }
public DbSet<PersistedGrant> PersistedGrants { get; set; }
public DbSet<DeviceFlowCodes> DeviceFlowCodes { get; set; }
Task<int> IPersistedGrantDbContext.SaveChangesAsync() => base.SaveChangesAsync();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.ConfigurePersistedGrantContext(_operationalStoreOptions.Value);
// Override default AspNet Identity table names
modelBuilder.Entity<ApplicationUser>().Property(x => x.Created).HasDefaultValueSql("getdate()");
modelBuilder.Entity<ApplicationUser>(entity => { entity.ToTable(name: "Users"); });
modelBuilder.Entity<ApplicationRole>(entity => { entity.ToTable(name: "Roles"); });
modelBuilder.Entity<DeviceFlowCodes>(entity => { entity.ToTable("UserDeviceCodes"); });
modelBuilder.Entity<PersistedGrant>(entity => { entity.ToTable("UserPersistedGrants"); });
modelBuilder.Entity<ApplicationUserRole>(entity =>
{
entity.ToTable("UserRoles");
entity.HasKey(x => new { x.UserId, x.RoleId });
entity.HasOne(ur => ur.Role)
.WithMany(r => r.UserRoles)
.HasForeignKey(ur => ur.RoleId)
.IsRequired();
entity.HasOne(ur => ur.User)
.WithMany(r => r.UserRoles)
.HasForeignKey(ur => ur.UserId)
.IsRequired();
});
modelBuilder.Entity<IdentityUserClaim<string>>(entity => { entity.ToTable("UserClaims"); });
modelBuilder.Entity<IdentityUserLogin<string>>(entity => { entity.ToTable("UserLogins"); });
modelBuilder.Entity<IdentityUserToken<string>>(entity => { entity.ToTable("UserTokens"); });
modelBuilder.Entity<IdentityRoleClaim<string>>(entity => { entity.ToTable("RoleClaims"); });
}
}
In my Startup.cs file:
// ASP.NET Identity
services.AddDefaultIdentity<ApplicationUser>(options =>
{
options.User.RequireUniqueEmail = true;
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(10);
})
.AddRoles<ApplicationRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
// Identity Server
var identityServerBuilder = services.AddIdentityServer(options =>
{
options.Authentication.CookieSlidingExpiration = true;
options.Authentication.CookieLifetime = TimeSpan.FromDays(7);
}).AddAspNetIdentity<ApplicationUser>()
.AddOperationalStore<ApplicationDbContext>()
.AddIdentityResources()
.AddApiResources()
.AddClients();

Related

.NET Core API Endpoint gives 404 only in deployed version

I am building a .NET Core (3.1) Web API which is being hosted in IIS.
I have 2 endpoints:
/api/status
/api/widget/config/{id}
Both endpoints work perfectly when running locally. The /api/status endpoint works in my deployed version too. But the other endpoint gives a 404 error in the deployed version. As it works locally, I believe this to be an issue with how it is deployed. Please can you help me understand the issue?
Here are my 2 controllers code:
[Route("api/[controller]")]
[ApiController]
public class StatusController : ControllerBase
{
[HttpGet]
public ActionResult Get()
{
return Ok("API is available");
}
}
and
[Route("api/[controller]")]
[ApiController]
public class WidgetController : ControllerBase
{
private readonly IWidgetService service;
public WidgetController(IWidgetService _service)
{
service = _service;
}
[HttpGet]
[Route("~/api/[controller]/[action]/{id}")]
public ActionResult Config(Guid id)
{
return Ok(service.GetWidgetConfig(id));
}
}
and below is my Program.cs and Startup.cs:
public static void Main(string[] args)
{
var host = CreateHostBuilder(args).Build();
using (var scope = host.Services.CreateScope())
{
var services = scope.ServiceProvider;
try
{
SeedDatabase.Initialize(services);
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occured seeding the DB");
}
}
host.Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseKestrel();
webBuilder.UseContentRoot(Directory.GetCurrentDirectory());
webBuilder.UseIIS();
webBuilder.UseStartup<Startup>();
});
and
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(opts => opts.UseSqlServer(Configuration.GetConnectionString("sqlConnection"),
options => options.MigrationsAssembly("MyProject")));
services.AddIdentity<ApplicationUser, IdentityRole>(opt =>
{
opt.Password.RequiredLength = 8;
opt.Password.RequireDigit = true;
opt.Password.RequireUppercase = true;
opt.Password.RequireNonAlphanumeric = true;
opt.SignIn.RequireConfirmedAccount = false;
opt.SignIn.RequireConfirmedAccount = false;
opt.SignIn.RequireConfirmedPhoneNumber = false;
}).AddEntityFrameworkStores<ApplicationDbContext>();
services.AddScoped<IWidgetService, WidgetService>();
services.AddCors(o => o.AddPolicy("CorsPolicy", builder => {
builder
.WithMethods("GET", "POST")
.AllowAnyHeader()
.AllowAnyOrigin();
}));
services.AddMvc()
.AddNewtonsoftJson(options => options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore);
}
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.UseCors("CorsPolicy");
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
Change your controller code to this:
[Route("api/[controller]")]
[ApiController]
public class WidgetController : ControllerBase
{
private readonly IWidgetService service;
public WidgetController(IWidgetService _service)
{
service = _service;
}
[HttpGet("Config/{id}")]
public ActionResult Config(Guid id)
{
return Ok(service.GetWidgetConfig(id));
}
}
Change your code like below:-
Startup.cs
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
Controller:-
[ApiController]
[Route("api/[controller]")]
public class WidgetController : ControllerBase
{
private readonly IWidgetService service;
public WidgetController(IWidgetService _service)
{
service = _service;
}
[HttpGet("Config/{id}")]
public ActionResult Config(Guid id)
{
return Ok(service.GetWidgetConfig(id));
}
}
Also try your write connection string in appsettings.Development.json file.
It will resolve your issue.

can odata and non odata routes co exists

i am using asp.net core 3.x and i have installed .net core's odata nuget.
in my services section i have setup odata like this
services.AddOData();
services.AddODataQueryFilter();
services.AddMvc(options =>
{
options.EnableEndpointRouting = false;
});
and configuration looks like this.
app.UseMvc(b =>
{
b.MapODataServiceRoute("odata", "odata", GetEdmModel());
});
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
when i use /odata/Accounts i get odata response
is it possible to get /api/Accounts at the same time ?
//[ODataRoutePrefix("Account")]
[Route("api/[controller]")]
[ApiController]
public class AccountsController : ControllerBase
{
private readonly IAccountService _accountService;
public AccountsController(IAccountService accountService)
{
_accountService = accountService;
}
[ODataRoute]
[EnableQuery(PageSize = 2, AllowedQueryOptions = AllowedQueryOptions.All)]
[HttpGet]
public async Task<ActionResult<IEnumerable<Account>>> Get()
{
return Ok(await _accountService.GetAllAccounts());
}
}
Odata may face errors using $select from .net core 3.x using System.Text.Json serializer . So that in your scenario you can use AddNewtonsoftJson as workaround . Note: If the AddNewtonsoftJson method isn't available, make sure that you installed the Microsoft.AspNetCore.Mvc.NewtonsoftJson package . Code sample below is for your reference :
Student.cs :
public class Student
{
public Guid Id { get; set; }
public string Name { get; set; }
public int Score { get; set; }
}
StudentsController.cs :
[Route("api/[controller]")]
[ApiController]
public class StudentsController : ControllerBase
{
[HttpGet]
[EnableQuery()]
public IEnumerable<Student> Get()
{
return new List<Student>
{
CreateNewStudent("Cody Allen", 130),
CreateNewStudent("Todd Ostermeier", 160),
CreateNewStudent("Viral Pandya", 140)
};
}
private static Student CreateNewStudent(string name, int score)
{
return new Student
{
Id = Guid.NewGuid(),
Name = name,
Score = score
};
}
}
ConfigureServices:
services.AddControllers(mvcOptions =>
mvcOptions.EnableEndpointRouting = false)
.AddNewtonsoftJson();
services.AddOData();
services.AddODataQueryFilter();
Configure :
app.UseMvc(b =>
{
b.Select().Filter();
b.MapODataServiceRoute("odata", "odata", GetEdmModel());
b.EnableDependencyInjection(); //add this line
});
GetEdmModel :
IEdmModel GetEdmModel()
{
var odataBuilder = new ODataConventionModelBuilder();
odataBuilder.EntitySet<Student>("Students");
return odataBuilder.GetEdmModel();
}
So that both https://localhost:44334/odata/students?$select=name and https://localhost:44334/api/students?$select=namewould return data .

blazor webassembly System.Net.Http.HttpRequestException: Response status code does not indicate success: 400 (Bad Request)

I have a blazor project that is dotnet core hosted. I am working on blazor authentication following this tutorial. Everything is ok on the server side because I was able to use Postman to create users successfully. I have tried different suggestions online but non work for me. Some suggested CORS which I fixed, some route which I amended but problem still persist.
I have been having issue debugging as I cant get break point in the browser console. I have debugged some blazor project I could set break points and view local variables but I couldnt with the current project. I dont know if its a bug.
server startup.cs
namespace EssentialShopCoreBlazor.Server
{
public class Startup
{
public IConfiguration Configuration { get; }
public Startup(IConfiguration _configuration)
{
Configuration = _configuration;
}
readonly string AllowSpecificOrigins = "allowSpecificOrigins";
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<DbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddIdentity<IdentityUsersModel, IdentityRole>()
.AddEntityFrameworkStores<DbContext>()
.AddDefaultTokenProviders();
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options =>{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = Configuration["JwtIssuer"],
ValidAudience = Configuration["JwtAudience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JwtSecurityKey"]))
};
});
services.AddCors(options =>
{
options.AddPolicy(AllowSpecificOrigins,
builder =>
{
builder.WithOrigins("https://localhost:44365", "https://localhost:44398")
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
});
});
services.AddScoped<AccountAuthController>();
services.AddMvc().AddNewtonsoftJson();
services.AddResponseCompression(opts =>
{
opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
new[] { "application/octet-stream" });
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseResponseCompression();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBlazorDebugging();
}
app.UseCors(AllowSpecificOrigins);
app.UseStaticFiles();
app.UseClientSideBlazorFiles<Client.Startup>();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapDefaultControllerRoute();
endpoints.MapFallbackToClientSideBlazor<Client.Startup>("index.html");
});
}
}
}
controller
namespace EssentialShopCoreBlazor.Server.Controllers
{
[Route("essential/users/")]
public class AccountAuthController : ControllerBase
{
private static UserModel LoggedOutUser = new UserModel { IsAuthenticated = false };
private readonly UserManager<IdentityUsersModel> userManager;
private readonly IConfiguration configuration;
private readonly SignInManager<IdentityUsersModel> signInManager;
public AccountAuthController(UserManager<IdentityUsersModel> _userManager, SignInManager<IdentityUsersModel> _signInManager, IConfiguration _configuration)
{
userManager = _userManager;
signInManager = _signInManager;
configuration = _configuration;
}
[HttpPost]
[Route("create")]
public async Task<ActionResult<ApplicationUserModel>> CreateUser([FromBody] ApplicationUserModel model)
{
var NewUser = new IdentityUsersModel
{
UserName = model.UserName,
BusinessName = model.BusinessName,
Email = model.Email,
PhoneNumber = model.PhoneNumber
};
var result = await userManager.CreateAsync(NewUser, model.Password);
if (!result.Succeeded)
{
var errors = result.Errors.Select(x => x.Description);
return BadRequest(new RegistrationResult { Successful = false, Errors = errors });
}
return Ok(new RegistrationResult { Successful = true });
}
}}
client service
public class AuthService : IAuthService
{
private readonly HttpClient _httpClient;
private readonly AuthenticationStateProvider _authenticationStateProvider;
private readonly ILocalStorageService _localStorage;
string BaseUrl = "https://localhost:44398/essential/users/";
public AuthService(HttpClient httpClient,
AuthenticationStateProvider authenticationStateProvider,
ILocalStorageService localStorage)
{
_httpClient = httpClient;
_authenticationStateProvider = authenticationStateProvider;
_localStorage = localStorage;
}
public async Task<RegistrationResult> Register(ApplicationUserModel registerModel)
{
var result = await _httpClient.PostJsonAsync<RegistrationResult>(BaseUrl + "create", registerModel);
return result;
}
}

GraphQL authentication with Asp.net core using JWT

I am using for GraphQL for .NET package for graphql. But I couldn't understand how can I authentication with JWT in graphql query or mutation.
I read the guide about authorization but I couldn't accomplish.
I need help with GraphQL for .NET authentication.
Any help will be appreciated.
Thanks
The guide is around authorization. The step you're looking for is the authentication and since graphql can be implemented using a ASP.Net API controller, you can implement JWT authentication as you would with any controller.
Here is a sample grapql controller using an Authorize attribute. You could, however, implement this using filter or if you want full control, custom middleware.
[Route("api/[controller]")]
[ApiController]
[Authorize]
public class GraphQLController : ControllerBase
{
private readonly IDocumentExecuter executer;
private readonly ISchema schema;
public GraphQLController(IDocumentExecuter executer, ISchema schema)
{
this.executer = executer;
this.schema = schema;
}
[HttpPost]
public async Task<ActionResult<object>> PostAsync([FromBody]GraphQLQuery query)
{
var inputs = query.Variables.ToInputs();
var queryToExecute = query.Query;
var result = await executer.ExecuteAsync(o => {
o.Schema = schema;
o.Query = queryToExecute;
o.OperationName = query.OperationName;
o.Inputs = inputs;
o.ComplexityConfiguration = new GraphQL.Validation.Complexity.ComplexityConfiguration { MaxDepth = 15};
o.FieldMiddleware.Use<InstrumentFieldsMiddleware>();
}).ConfigureAwait(false);
return this.Ok(result);
}
}
public class GraphQLQuery
{
public string OperationName { get; set; }
public string Query { get; set; }
public Newtonsoft.Json.Linq.JObject Variables { get; set; }
}
In the Startup.cs I have configured JWT bearer token authentication.
Hope this helps.
I myself struggled for two days as well. I'm using https://github.com/graphql-dotnet/authorization now with the setup from this comment (from me): https://github.com/graphql-dotnet/authorization/issues/63#issuecomment-553877731
In a nutshell, you have to set the UserContext for the AuthorizationValidationRule correctly, like so:
public class Startup
{
public virtual void ConfigureServices(IServiceCollection services)
{
...
services.AddGraphQLAuth(_ =>
{
_.AddPolicy("AdminPolicy", p => p.RequireClaim("Role", "Admin"));
});
services.AddScoped<IDependencyResolver>(x => new FuncDependencyResolver(x.GetRequiredService));
services.AddScoped<MySchema>();
services
.AddGraphQL(options => { options.ExposeExceptions = true; })
.AddGraphTypes(ServiceLifetime.Scoped);
...
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IServiceProvider serviceProvider)
{
...
app.UseMiddleware<MapRolesForGraphQLMiddleware>(); // optional, only when you don't have a "Role" claim in your token
app.UseGraphQL<MySchema>();
...
}
}
public static class GraphQLAuthExtensions
{
public static void AddGraphQLAuth(this IServiceCollection services, Action<AuthorizationSettings> configure)
{
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddSingleton<IAuthorizationEvaluator, AuthorizationEvaluator>();
services.AddTransient<IValidationRule, AuthorizationValidationRule>();
services.AddTransient<IUserContextBuilder>(s => new UserContextBuilder<GraphQLUserContext>(context =>
{
var userContext = new GraphQLUserContext
{
User = context.User
};
return Task.FromResult(userContext);
}));
services.AddSingleton(s =>
{
var authSettings = new AuthorizationSettings();
configure(authSettings);
return authSettings;
});
}
}
public class GraphQLUserContext : IProvideClaimsPrincipal
{
public ClaimsPrincipal User { get; set; }
}
public class MapRolesForGraphQLMiddleware
{
private readonly RequestDelegate _next;
public MapRolesForGraphQLMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
// custom mapping code to end up with a "Role" claim
var metadata = context.User.Claims.SingleOrDefault(x => x.Type.Equals("metadata"));
if (metadata != null)
{
var roleContainer = JsonConvert.DeserializeObject<RoleContainer>(metadata.Value);
(context.User.Identity as ClaimsIdentity).AddClaim(new Claim("Role", string.Join(", ", roleContainer.Roles)));
}
await _next(context);
}
}
public class RoleContainer
{
public String[] Roles { get; set; }
}

Method FindByLoginAsync doesn't work correctly with AspNetCore.Identity

I have used the AspNetCore.Identity in my Asp.Net Core application and I want to call the method called FindByLoginAsync but result is always NULL.
Versions:
Microsoft.AspNetCore.Identity.EntityFrameworkCore (1.1.1)
Microsoft.AspNetCore.Identity (1.1.1)
Code:
var loginProvider = "Github"
var providerKey = "1234567";
var user = await _userManager.FindByLoginAsync(loginProvider, providerKey);
This record exists in the database, but this method returns always NULL.
I've tried trace the SQL query and I've got this:
exec sp_executesql N'SELECT TOP(1) [e].[ProviderKey], [e].[LoginProvider], [e].[ProviderDisplayName], [e].[UserId]
FROM [UserLogins] AS [e]
WHERE ([e].[ProviderKey] = #__get_Item_0) AND ([e].[LoginProvider] = #__get_Item_1)',N'#__get_Item_0 nvarchar(450),#__get_Item_1 nvarchar(450)',#__get_Item_0=N'Github',#__get_Item_1=N'1234567'
My SQL query is like [e].[LoginProvider] the value providerKey and [e].[ProviderKey] the value loginProvider.
Application DbContext
public class ApplicationDbContext : IdentityDbContext<ApplicationUser, ApplicationRole, int>
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Entity<ApplicationUser>(i =>
{
i.ToTable("Users");
i.HasKey(x => x.Id);
});
builder.Entity<ApplicationRole>(i =>
{
i.ToTable("Roles");
i.HasKey(x => x.Id);
});
builder.Entity<IdentityUserRole<int>>(i =>
{
i.ToTable("UserRoles");
i.HasKey(x => new { x.RoleId, x.UserId });
});
builder.Entity<IdentityUserLogin<int>>(i =>
{
i.ToTable("UserLogins");
i.HasKey(x => new { x.ProviderKey, x.LoginProvider });
});
builder.Entity<IdentityRoleClaim<int>>(i =>
{
i.ToTable("RoleClaims");
i.HasKey(x => x.Id);
});
builder.Entity<IdentityUserClaim<int>>(i =>
{
i.ToTable("UserClaims");
i.HasKey(x => x.Id);
});
builder.Entity<IdentityUserToken<int>>(i =>
{
i.ToTable("UserTokens");
i.HasKey(x => x.UserId);
});
}
}
Implementation of IdentityUser, IdentityRole
public class ApplicationUser : IdentityUser<int>
{
}
public class ApplicationRole : IdentityRole<int>
{
}
How can I fix this? How is this behaviour possible?
You have incorrect order of primary keys in registration of entity IdentityUserLogin. Change it to this
builder.Entity<IdentityUserLogin<int>>(i =>
{
i.ToTable("UserLogins");
i.HasKey(x => new { x.LoginProvider, x.ProviderKey });
});
That's the fix, now the rationale behind.
In version 1.1.1 the method UserStore.FindByLoginAsync used method DbSet.FindAsync, which accepts ordered array of values for primary keys. The order must follow the order used in entity registration.
public async virtual Task<TUser> FindByLoginAsync(string loginProvider, string providerKey,
CancellationToken cancellationToken = default(CancellationToken))
{
...
var userLogin = await UserLogins.FindAsync(new object[] { loginProvider, providerKey }, cancellationToken);
...
}
In the default implementation the primary keys are registered in correct order
builder.Entity<TUserLogin>(b =>
{
b.HasKey(l => new { l.LoginProvider, l.ProviderKey });
b.ToTable("AspNetUserLogins");
});