« Asp.net core identity » : différence entre les versions

De Banane Atomic
Aller à la navigationAller à la recherche
 
 
(22 versions intermédiaires par le même utilisateur non affichées)
Ligne 1 : Ligne 1 :
[[Category:.NET Core]]
[[Category:.NET Core]]
= Stocker les identités dans la bdd =
= Links =
<filebox fn='Data/Entities/StoreUser.cs'>
* [https://docs.microsoft.com/en-us/aspnet/core/security/authentication/customize-identity-model Identity model customization in ASP.NET Core]
public class StoreUser : IdentityUser
* [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]
    // IdentityUser contient déjà UserName, Email
}
</filebox>


<filebox fn='Data/MyAppContext.cs'>
= Update the database to store the identities =
// remplacer DbContext par IdentityDbContext<StoreUser>
<filebox fn='DataAccess/MyAppContext.cs'>
public class MyAppContext : IdentityDbContext<StoreUser>
// replace DbContext by IdentityDbContext<IdentityUser>
// use IdentityUserContext<IdentityUser> to have Identity without roles
public sealed class MyAppContext : IdentityDbContext<IdentityUser>
{ }
{ }
</filebox>
</filebox>


<kode lang='powershell'>
<kode lang='powershell'>
# créé le fichier Migrations/xxx_Identity.cs contenant les modifications
# create the migration file Migrations/xxx_Identity.cs
dotnet ef add migrations Identity
dotnet ef add migrations Identity
# mise à jour de la bdd
 
# 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 55 : 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<StoreUser, IdentityRole>(cfg =>
     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)
     {
     {
         cfg.User.RequireUniqueEmail = true;
         this.userManager = userManager;
         cfg.Password.RequireDigit = true;
        this.roleManager = roleManager;
     })
         this.config = config;
    .AddEntityFrameworkStores<MyAppContext>();  // get identity from an EF store
     }


    [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();
     // UseAuthentication doit de trouver avant UseMvc
}
     app.UseMvc();
 
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 173 : 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(cfg =>
         .AddJwtBearer(options =>
         {
         {
             cfg.TokenValidationParameters = new TokenValidationParameters()
             options.TokenValidationParameters = new TokenValidationParameters
             {
             {
                 ValidIssuer = Configuration["Tokens:Issuer"],
                 ValidIssuer = Configuration["Tokens:Issuer"],
Ligne 190 : Ligne 357 :
</filebox>
</filebox>


<filebox fn='config.json'>
<filebox fn='appsettings.json'>
"Tokens": {
"JWT": {
   "Key": "MyKey",
   "Key": "MyKey",
   "Issuer": "localhost",
   "Issuer": "localhost",

Dernière version du 23 mars 2021 à 22:19

Links

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>
{ }
Powershell.svg
# 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
Json.svg
{
    "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
Json.svg
{
    "Username":"Billy",
    "Password":"P@ssw0rd!"
}