« Asp.net core identity » : différence entre les versions
Apparence
Aucun résumé des modifications |
|||
(20 versions intermédiaires par le même utilisateur non affichées) | |||
Ligne 1 : | Ligne 1 : | ||
[[Category:.NET | [[Category:ASP.NET]] | ||
= Links = | = Links = | ||
* [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://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 12 : | Ligne 14 : | ||
<kode lang='powershell'> | <kode lang='powershell'> | ||
# | # create the migration file Migrations/xxx_Identity.cs | ||
dotnet ef add migrations Identity | dotnet ef add migrations Identity | ||
# | # update the database | ||
dotnet ef database update | dotnet ef database update | ||
</kode> | </kode> | ||
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; | ||
Ligne 53 : | Ligne 58 : | ||
</filebox> | </filebox> | ||
= Configuration = | = Web API = | ||
== Web API configuration == | |||
<filebox fn='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; | |||
}); | |||
</filebox> | |||
== JWT Configuration == | |||
<filebox fn='Startup.cs'> | <filebox fn='Startup.cs'> | ||
public void ConfigureServices(IServiceCollection services) | public void ConfigureServices(IServiceCollection services) | ||
{ | { | ||
services.AddIdentity< | 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"])) | |||
}; | |||
}); | |||
</filebox> | |||
<filebox fn='appsettings.json'> | |||
"JWT": { | |||
"Key": "MyKey", | |||
"Issuer": "localhost", | |||
"Audience": "users" | |||
} | |||
</filebox> | |||
== Token controller == | |||
<filebox fn='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(); | |||
</filebox> | |||
Test avec PostMan: | |||
* POST | |||
* Body → raw + JSON | |||
<kode lang='json'> | |||
{ | |||
"Username":"Billy", | |||
"Password":"P@ssw0rd!" | |||
} | |||
</kode> | |||
== Restrictions == | |||
<filebox fn='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 | |||
</filebox> | |||
= Configuration = | |||
<filebox fn='Startup.cs'> | |||
public void Configure(IApplicationBuilder app, IHostingEnvironment env) | public void Configure(IApplicationBuilder app, IHostingEnvironment env) | ||
{ | { | ||
// must be placed before UseMvc | |||
app.UseAuthentication(); | 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; | |||
}); | |||
</filebox> | </filebox> | ||
Ligne 171 : | Ligne 336 : | ||
Pour les API, il vaut mieux utiliser OpenId, OAuth2 ou JWT | Pour les API, il vaut mieux utiliser OpenId, OAuth2 ou JWT | ||
== Configuration == | == Tokens Configuration == | ||
<filebox fn='Startup.cs'> | <filebox fn='Startup.cs'> | ||
public void ConfigureServices(IServiceCollection services) | public void ConfigureServices(IServiceCollection services) | ||
{ | { | ||
services.AddIdentity<IdentityUser, IdentityRole>() | |||
.AddEntityFrameworkStores<MyAppContext>() | |||
.AddDefaultTokenProviders(); | |||
services.AddAuthentication() | services.AddAuthentication() | ||
.AddCookie() | //.AddCookie() | ||
.AddJwtBearer( | .AddJwtBearer(options => | ||
{ | { | ||
options.TokenValidationParameters = new TokenValidationParameters | |||
{ | { | ||
ValidIssuer = Configuration["Tokens:Issuer"], | ValidIssuer = Configuration["Tokens:Issuer"], | ||
Ligne 188 : | Ligne 357 : | ||
</filebox> | </filebox> | ||
<filebox fn=' | <filebox fn='appsettings.json'> | ||
" | "JWT": { | ||
"Key": "MyKey", | "Key": "MyKey", | ||
"Issuer": "localhost", | "Issuer": "localhost", |
Dernière version du 6 avril 2025 à 17:29
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!"
}
|