CS296N Web Development 2: ASP.NET             
                                

Authentication, Authorization and Role Management

Topics by week
1. Intro to course and Identity
6. Creating a Web Service, Async / Await
2. Authentication and Authorization 7. Consuming a Web Service
3. Security
8. Front-End Frameworks
4. Load Testing and Performance
9. Docker Containers
5. Publishing to Linux, Midterm 10. Microservices

Contents


Introduction

  • Discuss lab 1 PRs and code reviews
  • Review quiz 1

Review - Identity

  • Run BookInfo
  • Add a user, list the accounts
    • Registration
    • Login (for authentication)
    • Authorization (for particular controllers or controller methods)
    • Role management
    • Seed admin user

Textbook Example (Freeman, Ch. 29)

The example MVC app has the following features

  • /Home - Shows details on the currently logged in user
    • Index - requires login, has [Authorize] attribute
    • OtherAction - same as above, has [Authorize(Roles="Users")] attribute
  • /Admin - Shows a list of users and has buttons for:
    • Create - create a user account
    • Edit
    • Delete
  • /Account
    • Login
    • Logout
  • /RoleAdmin - Shows roles and the users in them and has buttons for:
    • Create - will only be useful after adding claims (Ch. 29)
    • Edit - use this to add a user to a role
    • Delete - will only be useful after adding claims (Ch. 29)

Adding Authentication and Authorization to Your Web App

Authorization: Login and Logout

In an account controller, add:

  • Login methods:

    [AllowAnonymous]
    public IActionResult Login(string returnUrl)
    {
      ViewBag.returnUrl = returnUrl;
      return View();
    }

    [HttpPost]
    [AllowAnonymous]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> Login(LoginViewModel model, string returnUrl)
    {
      if (ModelState.IsValid) {
        User user = await userManager.FindByEmailAsync(model.Email);
          if (user != null) {
            await signInManager.SignOutAsync();
            var result = await signInManager.PasswordSignInAsync(user, model.Password, false, false);
            if (result.Succeeded) {
              return Redirect(returnUrl ?? "/");
            }
          }
          ModelState.AddModelError(nameof(LoginViewModel.Email), "Invalid user or password");
        }
        return View(model);
    }

  • logout method:
     [Authorize]
    public async Task<IActionResult> Logout()
    {
    await signInManager.SignOutAsync();
    return RedirectToAction("Index", "Home");
    }
In views, add:
  • A login view
    @model LoginViewModel
    <h4>Log In</h4>
    <div class="text-danger" asp-validation-summary="All"></div>
    <form asp-action="Login" method="post">
      <input type="hidden" name="returnUrl" value="@ViewBag.returnUrl" />

      <label asp-for="Email"></label>
      <input asp-for="Email" />

      <label asp-for="Password"></label>
      <input asp-for="Password" />

      <button type="submit">Log In</button>
    </form>

  • A logout view x
  • Add the [Authorize] attribute to methods that require authorization

Authorization

The ASP.NET Core platform provides information about the user through the HttpContext object, which is used by the Authorize attribute to check the status of the current request and see whether the user has been authenticated.

Add the [Authorize] attributes to methods that require authorization
  • This will cause a redirect to Account/Login
  • And  a parameter for the return URL will be passed automatically. For example:
    ?ReturnUrl=%2FBook%2FAdd
    • NNote: %2F is the hex code for the forward slash, /

Authorization by Role

  1. Create default roles
    • In the application's DbContext class, add code to create the roles you want:

      const string ADMIN = "Admin";
      const string MEMBER = "Member";
      if (await roleManager.FindByNameAsync(ADMIN) == null)  {
          await roleManager.CreateAsync(new IdentityRole(ADMIN));
      }
      if (await roleManager.FindByNameAsync(MEMBER) == null)  {
          await roleManager.CreateAsync(new IdentityRole(MEMBER));
      }
      Note: In this example, I used constants for the role names. You may want to get the role names from your appsettings.json file, like the author of the textobook does:
      configuration["Data:AdminUser:Role"]

Seed the database with a temporary admin account

In order to avoid the chicken and egg problem of how do you create an admin accout if you have to be an admin to craet the accout, you can seed your Identity database with an admin account that you would remove or change as soon as you deploy your web app.

One way to do this is by storing the account credentials in appsettings.json, and then using them to create a user account at startup. For example:

appsettings.json - The account settings are stored here
"Data": {
  "AdminUser": {
    "Name": "Admin",
    "Email": "admin@example.com",
    "Password": "secret",
    "Role": "Admins"
},

AppDbContext.cs - The role and account are created here public static async Task CreateAdminAccount(IServiceProvider serviceProvider, IConfiguration configuration) {
  UserManager<User> userManager = serviceProvider.GetRequiredService<UserManager<User>>();
  RoleManager<IdentityRole> roleManager = serviceProvider.GetRequiredService<RoleManager<IdentityRole>>();
  string username = configuration["Data:AdminUser:Name"];
  string email = configuration["Data:AdminUser:Email"];
  string password = configuration["Data:AdminUser:Password"];
  string role = configuration["Data:AdminUser:Role"];
  if (await userManager.FindByNameAsync(username) == null) {
    if (await roleManager.FindByNameAsync(role) == null) {
      await roleManager.CreateAsync(new IdentityRole(role));
    }
    User user = new User {UserName = username, Email = email};
    IdentityResult result = await userManager.CreateAsync(user, password);
    if (result.Succeeded) {
      await userManager.AddToRoleAsync(user, role);
    }
  }
}

Startup.cs - The method defined above is called herepublic void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
  . . .
  // lines of code omitted, the line below is at the end of the method
  ApplicationDbContext.CreateAdminAccount(app.ApplicationServices, Configuration).Wait();
}

Authorize a controller or method by role

Add the attribute [Authorize(Role="SomeRole")] to a class or method.

If a user who is authenticated, but not in the correct role attempts to access this, they will be redirected to /Account/AccessDenied


Role Management

Create and edit user roles using the Role Manager class
Edit user roles to add a user


GitHub Repository: CS296N-BookInfo-Core-2 - UserRoles branch

Note:
 I discovered I needed to comment out the last two lines in Startup.Cofigure like this:

// SeedData.EnsurePopulated(app); 
// ApplicationDbContext.CreateAdminAccount(app.ApplicationServices, Configuration).Wait();
Then I could run dotnet ef database update successfully

After updating the database, I uncommented the lines and ran the app. When I ran the app the database was seeded and the Admin account created.

Web App Examples - App from the VS MVC project template

Getting the Current User

Example from the default Visual Studio MVC project with authentication, from the Manage controller:
[HttpGet]
public async Task<IActionResult> Index() { var user = await _userManager.GetUserAsync(User); if (user == null) { throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); } var model = new IndexViewModel { Username = user.UserName, Email = user.Email, PhoneNumber = user.PhoneNumber, IsEmailConfirmed = user.EmailConfirmed, StatusMessage = StatusMessage }; return View(model); }

Reading

Freeman, Pro ASP.NET Core MVC 2
  • Ch. 12 - SportsStore: Sections on Identity - Notes
  • Ch. 29 - Applying ASP.NET Core Identity: Authorizing users with roles
Microsoft ASP.NET Core MVC Tutorial

Conclusion

  • Review due dates on Moodle
  • Next time we will cover security