Claims Authorization Example

Startup.cs

using System.IO;
using System.Linq;
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Newtonsoft.Json;
 
namespace WebApplication25
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }
 
        public IConfiguration Configuration { get; }
 
        public void ConfigureServices(IServiceCollection services)
        {
            var cookieAuthSection = Configuration.GetSection("CookieAuth");
            string appName = cookieAuthSection.GetValue<string>("ApplicationName");
            string keyLocation = cookieAuthSection.GetValue<string>("KeyLocation");
            services.AddDataProtection()
                .SetApplicationName(appName)
                //.SetDefaultKeyLifetime(TimeSpan.FromDays(9999))
                .PersistKeysToFileSystem(new DirectoryInfo(keyLocation));
 
            services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
                .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, config =>
                {
                    config.Cookie.Name = "myapp";
                    config.LoginPath = "/Login";
                    config.AccessDeniedPath = "/AccessDenied";
                });
 
            services.AddAuthorization(config =>
            {
                config.AddPolicy("UserName", policyBuilder =>
                {
                    policyBuilder.RequireClaim(ClaimTypes.Name);
                });
                config.AddPolicy("Admin", policyBuilder =>
                {
                    policyBuilder.RequireClaim("IsAdmin");
                });
                config.AddPolicy("PhoenixAdmin", policyBuilder =>
                {
                    policyBuilder.RequireClaim("CityAdmin", "Phoenix");
                });
                config.AddPolicy("Zero", policyBuilder =>
                {
                    policyBuilder.RequireAssertion(context =>
                    {
                        var roleClaim = context.User.Claims.FirstOrDefault(x => x.Type == "Roles");
                        if (roleClaim == null) { return false; }
 
                        var roles = JsonConvert.DeserializeAnonymousType(roleClaim.Value, new[] { new { RoleCode = "", OrgID = 0 } });
                        return roles.Any(r => r.OrgID == 0);
                    });
                });
                config.AddPolicy("AdminOrCityAdmin", policyBuilder =>
                {
                    policyBuilder.RequireAssertion(context =>
                    {
                        return context.User.Claims.Where(x => x.Type == "IsAdmin" || x.Type == "CityAdmin").Any();
                    });
                });
            });
 
            services.AddRazorPages();
        }
 
        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Error");
            }
 
            app.UseStaticFiles();
 
            app.UseRouting();
 
            app.UseAuthentication();
            app.UseAuthorization();
 
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapRazorPages();
            });
        }
    }
}

appsettings.json

{
    "Logging": {
        "LogLevel": {
            "Default": "Information",
            "Microsoft": "Warning",
            "Microsoft.Hosting.Lifetime": "Information"
        }
    },
    "AllowedHosts": "*",
    "CookieAuth": {
        "ApplicationName": "MyApplication",
        "KeyLocation": "\\\\some-server\\some-share\\keys"
    }
}

_Layout.cshtml

@using System.Security.Claims
 
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - WebApplication25</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
    <link rel="stylesheet" href="~/css/site.css" />
</head>
<body>
    <header>
        <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
            <div class="container">
                <a class="navbar-brand" asp-area="" asp-page="/Index">WebApplication25</a>
                <button class="navbar-toggler" type="button" data-toggle="collapse" data-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 flex-sm-row-reverse">
                    <ul class="navbar-nav flex-grow-1">
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-page="/Index">Home</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-page="/Privacy">Privacy</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-page="/UserAccount">User Account</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-page="/Admin">Admin</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-page="/PhoenixAdmin">Phoenix Admin</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-page="/Zero">Zero</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-page="/AdminOrCityAdmin">Admin Or City Admin</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-page="/Reset">Reset</a>
                        </li>
                    </ul>
                </div>
            </div>
        </nav>
    </header>
    <div class="container">
        <main role="main" class="pb-3">
            @RenderBody()
        </main>
    </div>
 
    <div>
        Machine Name: @Environment.MachineName
    </div>
 
    @{
        var claimsIdentity = User?.Identity as ClaimsIdentity;
        if (claimsIdentity != null)
        {
            <ul>
                @foreach (var claim in claimsIdentity.Claims)
                {
                    <li>@claim.Type | @claim.Value</li>
                }
            </ul>
        }
    }
 
 
    <footer class="border-top footer text-muted">
        <div class="container">
            &copy; 2020 - WebApplication25 - <a asp-area="" asp-page="/Privacy">Privacy</a>
            Hello @User?.Identity?.Name
        </div>
    </footer>
 
    <script src="~/lib/jquery/dist/jquery.min.js"></script>
    <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
    <script src="~/js/site.js" asp-append-version="true"></script>
 
    @RenderSection("Scripts", required: false)
</body>
</html>

AccessDenied.cshtml

@page
@model WebApplication25.Pages.AccessDeniedModel
@{
    ViewData["Title"] = "AccessDenied";
}
 
<h1>AccessDenied</h1>

Login.cshtml

@page
@model WebApplication25.Pages.LoginModel
@{
    ViewData["Title"] = "Login";
}
 
<h1>Login</h1>
 
<form method="post">
    <input type="text" name="UserName" />
    <input type="password" name="Password" />
    <button type="submit">Submit</button>
</form>

Login.cshtml.cs

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
 
namespace WebApplication25.Pages
{
    public class LoginModel : PageModel
    {
        [BindProperty]
        public string UserName { get; set; }
        [BindProperty]
        public string Password { get; set; }
 
        public void OnGet()
        {
 
        }
 
        public async Task/*<IActionResult>*/ OnPostAsync()
        {
            if (string.IsNullOrWhiteSpace(UserName))
            {
                throw new ApplicationException("UserName is required");
            }
            var claims = new List<Claim> { new Claim(ClaimTypes.Name, UserName) };
 
            if (UserName == "admin") { claims.Add(new Claim("IsAdmin", "1")); }
 
            if (UserName == "phoenix") { claims.Add(new Claim("CityAdmin", "Phoenix")); }
 
            var rand = new Random();
            var roles = Enumerable.Range(0, 100).Select(i => new
            {
                RoleCode = Guid.NewGuid().ToString().Substring(0, 3),
                OrgID = rand.Next(0, 1000)
            }).ToList();
            if (UserName == "zero") { roles.Add(new { RoleCode = "ZZ", OrgID = 0 }); }
 
            string rolesJSON = JsonConvert.SerializeObject(roles);
            if (UserName == "manyroles") { claims.Add(new Claim("Roles", rolesJSON)); }
            if (UserName == "zero") { claims.Add(new Claim("Roles", rolesJSON)); }
 
            var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
            var principal = new ClaimsPrincipal(identity);
            await HttpContext.SignInAsync(principal).ConfigureAwait(false);
        }
    }
}

Admin.cshtml

@page
@model WebApplication25.Pages.AdminModel
@{
    ViewData["Title"] = "Admin";
}
 
<h1>Admin</h1>

Admin.cshtml.cs

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;
 
namespace WebApplication25.Pages
{
    [Authorize(Policy = "Admin")]
    public class AdminModel : PageModel
    {
        public void OnGet()
        {
 
        }
    }
}

Others

Repeat for AdminOrCityAdmin.cshtml, PhoenixAdmin.cshtml, UserAccount.cshtml, Zero.cshtml, with the appropriate Authorize attributes representing the policies.

Notes

Adding the key location and app name to the Startup file gives you the ability to put the keys in a file share and share them among multiple servers in a web farm. On a single server, those lines are unnecessary, and it uses the default cookie encryption.

There are definitely better ways to do things like roles, but this example shows that you can dump everything into claims, retrieve them, and validate them with attributes, without a whole lot of work.

Comments

Popular posts from this blog

C# Record Serialization

Versioned content in MVC

Add timestamp to photo using ImageMagick