« Asp.net core identity » : différence entre les versions
De Banane Atomic
Aller à la navigationAller à la recherche
(→Links) |
|||
(Une version intermédiaire par le même utilisateur non affichée) | |||
Ligne 3 : | Ligne 3 : | ||
* [https://docs.microsoft.com/en-us/aspnet/core/security/authentication/customize-identity-model Identity model customization in ASP.NET Core] | * [https://docs.microsoft.com/en-us/aspnet/core/security/authentication/customize-identity-model Identity model customization in ASP.NET Core] | ||
* [https://www.c-sharpcorner.com/article/authentication-and-authorization-in-asp-net-core-web-api-with-json-web-tokens/ Identity in web API with JWT] | * [https://www.c-sharpcorner.com/article/authentication-and-authorization-in-asp-net-core-web-api-with-json-web-tokens/ Identity in web API with JWT] | ||
* [https://docs.microsoft.com/en-us/aspnet/core/blazor/security Blazor authentication and authorization] | |||
= Update the database to store the identities = | = Update the database to store the identities = | ||
Ligne 22 : | Ligne 23 : | ||
It will create 4 SQL tables: {{boxx|AspNetUsers}} {{boxx|AspNetUserClaims}} {{boxx|AspNetUserLogins}} {{boxx|AspNetUserTokens}} | It will create 4 SQL tables: {{boxx|AspNetUsers}} {{boxx|AspNetUserClaims}} {{boxx|AspNetUserLogins}} {{boxx|AspNetUserTokens}} | ||
== Seed == | |||
<filebox fn='Data/MyAppSeeder.cs'> | <filebox fn='Data/MyAppSeeder.cs'> | ||
private readonly UserManager<IdentityUser> _userManager; | private readonly UserManager<IdentityUser> _userManager; |
Dernière version du 23 mars 2021 à 22:19
Links
- Identity model customization in ASP.NET Core
- Identity in web API with JWT
- Blazor authentication and authorization
Update the database to store the identities
DataAccess/MyAppContext.cs |
// replace DbContext by IdentityDbContext<IdentityUser> // use IdentityUserContext<IdentityUser> to have Identity without roles public sealed class MyAppContext : IdentityDbContext<IdentityUser> { } |
# create the migration file Migrations/xxx_Identity.cs dotnet ef add migrations Identity # update the database dotnet ef database update |
It will create 4 SQL tables: AspNetUsers AspNetUserClaims AspNetUserLogins AspNetUserTokens
Seed
Data/MyAppSeeder.cs |
private readonly UserManager<IdentityUser> _userManager; public MyAppSeeder(MyAppContext context, UserManager<IdentityUser> userManager) { _context = context; _userManager = userManager; } public async Task Seed() { _context.Database.EnsureCreated(); var user = await _userManager.FindByNameAsync("Billy"); if (user == null) { user = new IdentityUser("Billy"); var result = await _userManager.CreateAsync(user, "P@ssw0rd!"); if (result != IdentityResult.Success) { throw new InvalidOperationException("Failed to create the default user."); } |
Startup.cs |
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { using (var scope = app.ApplicationServices.CreateScope()) { var seeder = scope.ServiceProvider.GetService<MyAppSeeder>(); seeder.Seed().Wait(); // appel de Wait car Seed est maintenant asynchrone } |
Web API
Web API configuration
Startup.cs |
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, MyAppContext context) { app.UseAuthentication(); app.UseAuthorization(); } public void ConfigureServices(IServiceCollection services) { services.AddIdentity<IdentityUser, IdentityRole>() .AddEntityFrameworkStores<MyAppContext>(); // get identity from an EF store services.Configure<IdentityOptions>(options => { // Password settings options.Password.RequireDigit = true; options.Password.RequireLowercase = true; options.Password.RequireNonAlphanumeric = true; options.Password.RequireUppercase = true; options.Password.RequiredLength = 6; options.Password.RequiredUniqueChars = 1; // Lockout settings options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5); options.Lockout.MaxFailedAccessAttempts = 5; options.Lockout.AllowedForNewUsers = true; // User settings options.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+"; options.User.RequireUniqueEmail = false; }); |
JWT Configuration
Startup.cs |
public void ConfigureServices(IServiceCollection services) { services.AddIdentity<IdentityUser, IdentityRole>() .AddEntityFrameworkStores<MyAppContext>() .AddDefaultTokenProviders(); services.AddAuthentication() //.AddCookie() .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidIssuer = Configuration["Tokens:Issuer"], ValidAudience = Configuration["Tokens:Audience"], IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Tokens:Key"])) }; }); |
appsettings.json |
"JWT": { "Key": "MyKey", "Issuer": "localhost", "Audience": "users" } |
Token controller
Controllers/TokenController.cs |
[ApiController] [Route("[controller]")] public class TokenController : ControllerBase { private readonly UserManager<IdentityUser> userManager; private readonly RoleManager<IdentityRole> roleManager; private readonly IConfiguration config; public TokenController( UserManager<IdentityUser> userManager, RoleManager<IdentityRole> roleManager, IConfiguration config) { this.userManager = userManager; this.roleManager = roleManager; this.config = config; } [HttpPost] public async Task<IActionResult> CreateToken([FromBody] LoginDto login) { var user = await userManager.FindByNameAsync(login.Username); if (user != null) { if (await userManager.CheckPasswordAsync(user, login.Password)) { var claims = new[] { // unique string which represents the token new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), // name used for the mapping new Claim(JwtRegisteredClaimNames.UniqueName, user.UserName), }; var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config["JWT:key"])); var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var token = new JwtSecurityToken( config["JWT:Issuer"], config["JWT:Audience"], claims, expires: DateTime.UtcNow.AddMinutes(30), signingCredentials: credentials ); var wrappedToken = new { token = new JwtSecurityTokenHandler().WriteToken(token), expiration = token.ValidTo }; return Ok(wrappedToken); } } return Unauthorized(); |
Test avec PostMan:
- POST
- Body → raw + JSON
{ "Username":"Billy", "Password":"P@ssw0rd!" } |
Restrictions
Controllers/MyController |
[Authorize] // return 401 without a valid token [Authorize(Roles = UserRoles.Admin)] // return 403 without a valid token of a user having the admin role [ApiController] [Route("[controller]")] public class MyController : ControllerBase |
Configuration
Startup.cs |
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { // must be placed before UseMvc app.UseAuthentication(); } public void ConfigureServices(IServiceCollection services) { services.AddIdentity<IdentityUser, IdentityRole>() .AddEntityFrameworkStores<MyAppContext>(); // get identity from an EF store services.Configure<IdentityOptions>(options => { // Password settings options.Password.RequireDigit = true; options.Password.RequireLowercase = true; options.Password.RequireNonAlphanumeric = true; options.Password.RequireUppercase = true; options.Password.RequiredLength = 6; options.Password.RequiredUniqueChars = 1; // Lockout settings options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5); options.Lockout.MaxFailedAccessAttempts = 5; options.Lockout.AllowedForNewUsers = true; // User settings options.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+"; options.User.RequireUniqueEmail = false; }); |
Login View
Views/Account/Login.cshtml |
@model LoginViewModel @{ ViewBag.Title = "Login"; } @section scripts { <script src="~/vendor/jquery-validation/jquery.validate.min.js"></script> <script src="~/vendor/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script> } <div class="row"> <div class="col-md-4 col-md-offset-4"> <form method="post"> <div asp-validation-summary="ModelOnly" class="text-danger"></div> <div class="form-group"> <label asp-for="Username" class="control-label"></label> <input asp-for="Username" class="form-control" /> <span asp-validation-for="Username" class="text-danger"></span> </div> <div class="form-group"> <label asp-for="Password" class="control-label"></label> <input asp-for="Password" type="password" class="form-control" /> <span asp-validation-for="Password" class="text-danger"></span> </div> <div class="form-group"> <label asp-for="RememberMe" class="control-label">Remember me ?</label> <input asp-for="RememberMe" type="checkbox" class="checkbox-inline" /> </div> <div class="form-group"> <input type="submit" value="Login" class="btn btn-success" /> </div> </form> </div> </div> |
ViewModel/LoginViewModel.cs |
public class LoginViewModel { [Required] public string Username { get; set; } [Required] public string Password { get; set; } public bool RememberMe { get; set; } } |
Controllers/AccountController.cs |
public class AccountController : Controller { private readonly SignInManager<IdentityUser> _signInManager; public AccountController(SignInManager<IdentityUser> signInManager) { _signInManager = signInManager; } [HttpGet] public IActionResult Login() { if (this.User.Identity.IsAuthenticated) { return RedirectToAction("Index", "List"); } return View(); } [HttpPost] public IActionResult Login(LoginViewModel model) { if(ModelState.IsValid) { var result = await _signInManager.PasswordSignInAsync(model.Username, model.Password, model.RememberMe, false); if (result.Succeeded) { if (Request.Query.Keys.Contains("ReturnUrl")) { return Redirect(Request.Query["ReturnUrl"].First()); } else { RedirectToAction("Index", "List"); } } } // erreur générique ModelState.AddModelError("", "Failed to login"); // retour vers la vue de login return View(); } } |
Tokens
Par défaut l'authentification se fait avec des cookies.
Pour les API, il vaut mieux utiliser OpenId, OAuth2 ou JWT
Tokens Configuration
Startup.cs |
public void ConfigureServices(IServiceCollection services) { services.AddIdentity<IdentityUser, IdentityRole>() .AddEntityFrameworkStores<MyAppContext>() .AddDefaultTokenProviders(); services.AddAuthentication() //.AddCookie() .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidIssuer = Configuration["Tokens:Issuer"], ValidAudience = Configuration["Tokens:Audience"], IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Tokens:Key"])) }; }); |
appsettings.json |
"JWT": { "Key": "MyKey", "Issuer": "localhost", "Audience": "users" } |
Controllers/ItemsController.cs |
[Produces("application/json")] // force le format JSON [Route("api/[Controller]")] // /api/items [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] public class ItemsController : Controller |
Test avec PostMan:
- Headers
- Key: Authorization
- Bearer montoken
Création des tokens
Controllers/AccountController.cs |
private readonly SignInManager<IdentityUser> _signInManager; private readonly UserManager<IdentityUser> _userManager; private readonly IConfiguration _config; public AccountController(SignInManager<IdentityUser> signInManager, UserManager<IdentityUser> userManager, IConfiguration config) { _signInManager = signInManager; _userManager = userManager; _config = config; } [HttpPost] public async Task<IActionResult> CreateToken([FromBody]LoginViewModel model) { if (ModelState.IsValid) { var user = await _userManager.FindByNameAsync(model.Username); if (user!= null) { var result = await _signInManager.CheckPasswordSignInAsync(user, model.Password, false); if (result.Succeeded) { // créer le token var claims = new[] { // name of the subject //new Claim(JwtRegisteredClaimNames.Sub, user.Email), // unique string which represents the token new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), // name used for the mapping new Claim(JwtRegisteredClaimNames.UniqueName, user.UserName), }; var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Tokens:key"])); var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var token = new JwtSecurityToken( _config["Tokens:Issuer"], _config["Tokens:Audience"], claims, expires: DateTime.UtcNow.AddMinutes(30), signingCredentials: credentials ); var wrappedToken = new { token = new JwtSecurityTokenHandler().WriteToken(token), expiration = token.ValidTo }; return Created("", wrappedToken); } } } return BadRequest(); } |
Test avec PostMan:
- POST
- Body → raw + JSON
{ "Username":"Billy", "Password":"P@ssw0rd!" } |