Identityにroleを追加する

Identityにroleを追加する

Asp.Net Core MVCでプロジェクトを作り、Identityを追加するとDBにいくつかTableが作られるけど、Identityの設定ではRole追加が無い(らしい?)
「じゃ、Table作るなよ。」って感じもするけど、Roleはどうやって設定するのだろうかといろいろ検索した結果。自分で追加するらしい。

という事で、追加してみました。
Role用のContoroller、Viewを作ります。

参考

Adding Role Authorization to a ASP.NET MVC Core Application
How to work with Roles in ASP.NET Core Identity

製作

・VisualStudio2022
・Asp.Net Core Mvc

RoleSampleでプロジェクトを作りました。

Identityを追加

add-migration InitialMigration
update-database

マイグレーションするとDBはこんな感じになります。Identityの標準仕様らしい。

_Layout.cshtmlにログイン等のメニューを追加

<body>
  <header>
    <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
    <div class="container-fluid">
      <a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">RoleSample</a>
      <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent"
aria-expanded="false" aria-label="Toggle navigation">
        <span class="navbar-toggler-icon"></span>
      </button>
      <div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
        <ul class="navbar-nav flex-grow-1">
          <li class="nav-item">
            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
          </li>
          <li class="nav-item">
            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
          </li>
        </ul>
        <partial name="_LoginPartial" />
      </div>
    </div>
    </nav>
  </header>

実行して表示確認。

Role用のControllerとViewを作ります。

RoleController

using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using System.Linq;
using System.Threading.Tasks;

namespace RoleSample.Controllers
{
    public class RoleController : Controller
    {
        RoleManager roleManager;

        public RoleController(RoleManager roleManager)
        {
            this.roleManager = roleManager;
        }

        public IActionResult Index()
        {
            var roles = roleManager.Roles.ToList();
            return View(roles);
        }

        public IActionResult Create()
        {
            return View(new IdentityRole());
        }

        [HttpPost]
        public async Task Create(IdentityRole role)
        {
            await roleManager.CreateAsync(role);
            return RedirectToAction("Index");
        }
    }
}

ViewsにRoleフォルダを作りIndex.cshtmlとCreate.cshtmlを作ります。

Index.cshtml

@model IEnumerable<Microsoft.AspNetCore.Identity.IdentityRole>
@{
    ViewData["Title"] = "Role Index";
}

<h1>List of Roles</h1>

<p>
    <a asp-action="Create">Create</a>
</p>

<table class="table">
    <thead>
        <tr>
            <th>Id</th>
            <th>Name</th>
        </tr>
    </thead>
    <tbody>
        @foreach (var role in Model)
        {
            <tr>
                <td>@Html.DisplayFor(modelItem => role.Id)</td>
                <td>@Html.DisplayFor(modelItem => role.Name)</td>
            </tr>
        }
    </tbody>
</table>

Create.cshtml

@model Microsoft.AspNetCore.Identity.IdentityRole
@{
    ViewData["Title"] = "Role Create";
}

<h1>Create Role</h1>
<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Create">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="Name" class="control-label"></label>
                <input asp-for="Name" class="form-control" />
                <span asp-validation-for="Name" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-action="Index">Back to List</a>
</div>
@section Scripts {
    @{
        await Html.RenderPartialAsync("_ValidationScriptsPartial");
    }
}

_Layout.cshtmlにrole indexへのリンクを追加

Program.csを変更

using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using RoleSample.Data;
var builder = WebApplication.CreateBuilder(args);
var connectionString = builder.Configuration.GetConnectionString("RoleSampleContextConnection") ?? throw new InvalidOperationException("Connection string 'RoleSampleContextConnection' not found.");

builder.Services.AddDbContext(options =>
    options.UseSqlServer(connectionString));

//builder.Services.AddDefaultIdentity(options => options.SignIn.RequireConfirmedAccount = true)
//    .AddEntityFrameworkStores();

builder.Services.AddIdentity<IdentityUser, IdentityRole>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddDefaultUI()
    .AddEntityFrameworkStores()
    .AddDefaultTokenProviders();

// Add services to the container.
builder.Services.AddControllersWithViews();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    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.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.MapRazorPages();

app.Run();

実行すると

Roleの登録ができたら、identityのRegister.cshtmlとRegister.cs.html.csを変更します。

Register.cshtml.cs

namespace RoleSample.Areas.Identity.Pages.Account
{
    public class RegisterModel : PageModel
    {
        private readonly SignInManager _signInManager;
        private readonly UserManager _userManager;
        private readonly IUserStore _userStore;
        private readonly IUserEmailStore _emailStore;
        private readonly ILogger _logger;
        private readonly IEmailSender _emailSender;
        private readonly RoleManager _roleManager;

        public RegisterModel(
            UserManager userManager,
            IUserStore userStore,
            SignInManager signInManager,
            ILogger logger,
            IEmailSender emailSender,
            RoleManager roleManager)
        {
            _userManager = userManager;
            _userStore = userStore;
            _emailStore = GetEmailStore();
            _signInManager = signInManager;
            _logger = logger;
            _emailSender = emailSender;
            _roleManager = roleManager;
        }

        [BindProperty]
        public InputModel Input { get; set; }

        public string ReturnUrl { get; set; }

        public IList ExternalLogins { get; set; }

        public class InputModel
        {
            [Required]
            [EmailAddress]
            [Display(Name = "Email")]
            public string Email { get; set; }

            [Required]
            [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
            [DataType(DataType.Password)]
            [Display(Name = "Password")]
            public string Password { get; set; }

            [DataType(DataType.Password)]
            [Display(Name = "Confirm password")]
            [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
            public string ConfirmPassword { get; set; }

            // Role
            public string Rolename { get; set; }
        }


        public async Task OnGetAsync(string returnUrl = null)
        {
            ViewData["roles"] = _roleManager.Roles.ToList();
            ReturnUrl = returnUrl;
            ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
        }

        public async Task OnPostAsync(string returnUrl = null)
        {
            returnUrl ??= Url.Content("~/");
            var role = _roleManager.FindByIdAsync(Input.Rolename).Result;
            ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
            if (ModelState.IsValid)
            {
                var user = CreateUser();

                await _userStore.SetUserNameAsync(user, Input.Email, CancellationToken.None);
                await _emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None);
                var result = await _userManager.CreateAsync(user, Input.Password);

                if (result.Succeeded)
                {
                    _logger.LogInformation("User created a new account with password.");
                    await _userManager.AddToRoleAsync(user, role.Name);

                    var userId = await _userManager.GetUserIdAsync(user);
                    var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
                    code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
                    var callbackUrl = Url.Page(
                        "/Account/ConfirmEmail",
                        pageHandler: null,
                        values: new { area = "Identity", userId = userId, code = code, returnUrl = returnUrl },
                        protocol: Request.Scheme);

                    await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
                        $"Please confirm your account by clicking here.");

                    if (_userManager.Options.SignIn.RequireConfirmedAccount)
                    {
                        return RedirectToPage("RegisterConfirmation", new { email = Input.Email, returnUrl = returnUrl });
                    }
                    else
                    {
                        await _signInManager.SignInAsync(user, isPersistent: false);
                        return LocalRedirect(returnUrl);
                    }
                }
                foreach (var error in result.Errors)
                {
                    ModelState.AddModelError(string.Empty, error.Description);
                }
            }

            ViewData["roles"] = _roleManager.Roles.ToList();

            // If we got this far, something failed, redisplay form
            return Page();
        }



Register.cshtml

@page
@model RegisterModel
@{
    ViewData["Title"] = "Register";
    var roles = (List<IdentityRole>)ViewData["roles"];
}

<h1>@ViewData["Title"]</h1>

<div class="row">
    <div class="col-md-4">
        <form id="registerForm" asp-route-returnUrl="@Model.ReturnUrl" method="post">
            <h2>Create a new account.</h2>
            <hr />
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-floating">
                <input asp-for="Input.Email" class="form-control" autocomplete="username" aria-required="true" />
                <label asp-for="Input.Email"></label>
                <span asp-validation-for="Input.Email" class="text-danger"></span>
            </div>
            <div class="form-floating">
                <input asp-for="Input.Password" class="form-control" autocomplete="new-password" aria-required="true" />
                <label asp-for="Input.Password"></label>
                <span asp-validation-for="Input.Password" class="text-danger"></span>
            </div>
            <div class="form-floating">
                <input asp-for="Input.ConfirmPassword" class="form-control" autocomplete="new-password" aria-required="true" />
                <label asp-for="Input.ConfirmPassword"></label>
                <span asp-validation-for="Input.ConfirmPassword" class="text-danger"></span>
            </div>
            <div class="form-floating">
                <label asp-for="Input.Rolename"></label>
                <select asp-for="Input.Rolename" class="form-control" asp-items="@(new SelectList(roles, "Id", "Name"))"></select>
                <span asp-validation-for="Input.ConfirmPassword" class="text-danger"></span>
            </div>
            <button id="registerSubmit" type="submit" class="w-100 btn btn-lg btn-primary">Register</button>
        </form>
    </div>
    <div class="col-md-6 col-md-offset-2">
        <section>
            <h3>Use another service to register.</h3>
            <hr />
            @{
                if ((Model.ExternalLogins?.Count ?? 0) == 0)
                {
                        <div>
                            <p>
                                There are no external authentication services configured. See this <a href="https://go.microsoft.com/fwlink/?LinkID=532715">article
                                about setting up this ASP.NET application to support logging in via external services</a>.
                            </p>
                        </div>
                }
                else
                {
                        <form id="external-account" asp-page="./ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" method="post" class="form-horizontal">
                            <div>
                                <p>
                                    @foreach (var provider in Model.ExternalLogins)
                                {
                                        <button type="submit" class="btn btn-primary" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button>
                                }
                                </p>
                            </div>
                        </form>
                }
            }
        </section>
    </div>
</div>

@section Scripts {
    <partial name="_ValidationScriptsPartial" />
}

DB Tableは、Users

UserRoles

Roles

この後はコントローラやアクションで「ロールベースの承認」を入れてロール認証はOKなはず。

ASP.NET Core でのロール ベースの認可 | Microsoft Learn

[Authorize(Roles = "Administrator")]
public class AdministrationController : Controller
{
    public IActionResult Index() =>
        Content("Administrator");
}

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です