Blazor 5x - قسمت 31 - احراز هویت و اعتبارسنجی کاربران Blazor WASM - بخش 1 - انجام تنظیمات اولیه
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: دوازده دقیقه

در قسمت قبل، امکان سفارش یک اتاق را به همراه پرداخت آنلاین آن، به برنامه‌ی Blazor WASM این سری اضافه کردیم؛ اما ... هویت کاربری که مشغول انجام اینکار است، هنوز مشخص نیست. بنابراین در این قسمت می‌خواهیم مباحثی مانند ثبت نام و ورود به سیستم را تکمیل کنیم. البته مقدمات سمت سرور این بحث را در مطلب «Blazor 5x - قسمت 25 - تهیه API مخصوص Blazor WASM - بخش 2 - تامین پایه‌ی اعتبارسنجی و احراز هویت»، بررسی کردیم.


ارائه‌ی AuthenticationState به تمام کامپوننت‌های یک برنامه‌ی Blazor WASM

در قسمت 22، با مفاهیم CascadingAuthenticationState و AuthorizeRouteView در برنامه‌های Blazor Server آشنا شدیم؛ این مفاهیم در اینجا نیز یکی هستند:
- کامپوننت CascadingAuthenticationState سبب می‌شود AuthenticationState (لیستی از Claims کاربر)، به تمام کامپوننت‌های یک برنامه‌یBlazor  ارسال شود. در مورد پارامترهای آبشاری، در قسمت نهم این سری بیشتر بحث شد و هدف از آن، ارائه‌ی یکسری اطلاعات، به تمام زیر کامپوننت‌های یک کامپوننت والد است؛ بدون اینکه نیاز باشد مدام این پارامترها را در هر زیر کامپوننتی، تعریف و تنظیم کنیم. همینقدر که آن‌ها را در بالاترین سطح سلسله مراتب کامپوننت‌های تعریف شده تعریف کردیم، در تمام زیر کامپوننت‌های آن نیز در دسترس خواهند بود.
- کامپوننت AuthorizeRouteView امکان محدود کردن دسترسی به صفحات مختلف برنامه‌ی Blazor را بر اساس وضعیت اعتبارسنجی و نقش‌های کاربر جاری، میسر می‌کند.

روش اعمال این دو کامپوننت نیز یکی است و نیاز به ویرایش فایل BlazorWasm.Client\App.razor در اینجا وجود دارد:
<CascadingAuthenticationState>
    <Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true">
        <Found Context="routeData">
            <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
                <Authorizing>
                    <p>Please wait, we are authorizing the user.</p>
                </Authorizing>
                <NotAuthorized>
                    <p>Not Authorized</p>
                </NotAuthorized>
            </AuthorizeRouteView>
        </Found>
        <NotFound>
                <LayoutView Layout="@typeof(MainLayout)">
                    <p>Sorry, there's nothing at this address.</p>
                </LayoutView>
        </NotFound>
    </Router>
</CascadingAuthenticationState>
کامپوننت CascadingAuthenticationState، اطلاعات AuthenticationState را در اختیار تمام کامپوننت‌های برنامه قرار می‌دهد و کامپوننت AuthorizeRouteView، امکان نمایش یا عدم نمایش قسمتی از صفحه را بر اساس وضعیت لاگین شخص و یا محدود کردن دسترسی بر اساس نقش‌ها، میسر می‌کند.


مشکل! برخلاف برنامه‌های Blazor Server، برنامه‌های Blazor WASM به صورت پیش‌فرض به همراه تامین کننده‌ی توکار AuthenticationState نیستند.

اگر سری Blazor جاری را از ابتدا دنبال کرده باشید، کاربرد AuthenticationState را در برنامه‌های Blazor Server، در قسمت‌های 21 تا 23، پیشتر مشاهده کرده‌اید. همان مفاهیم، در برنامه‌های Blazor WASM هم قابل استفاده هستند؛ البته در اینجا به علت جدا بودن برنامه‌ی سمت کلاینت WASM Blazor، از برنامه‌ی Web API سمت سرور، نیاز است یک تامین کننده‌ی سمت کلاینت AuthenticationState را بر اساس JSON Web Token دریافتی از سرور، تشکیل دهیم و برخلاف برنامه‌های Blazor Server، این مورد به صورت خودکار مدیریت نمی‌شود و با ASP.NET Core Identity سمت سروری که JWT تولید می‌کند، یکپارچه نیست.
بنابراین در اینجا نیاز است یک AuthenticationStateProvider سفارشی سمت کلاینت را تهیه کنیم که بر اساس JWT دریافتی از Web API کار می‌کند. به همین جهت در ابتدا یک JWT Parser را طراحی می‌کنیم که رشته‌ی JWT دریافتی از سرور را تبدیل به <IEnumerable<Claim می‌کند. سپس این لیست را در اختیار یک AuthenticationStateProvider سفارشی قرار می‌دهیم تا اطلاعات مورد نیاز کامپوننت‌های CascadingAuthenticationState و AuthorizeRouteView تامین شده و قابل استفاده شوند.


نیاز به یک JWT Parser

در قسمت 25، پس از لاگین موفق، یک JWT تولید می‌شود که به همراه قسمتی از مشخصات کاربر است. می‌توان محتوای این توکن را در سایت jwt.io مورد بررسی قرار داد که برای نمونه به این خروجی می‌رسیم و حاوی claims تعریف شده‌است:
{
  "iss": "https://localhost:5001/",
  "iat": 1616396383,
  "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": "vahid@dntips.ir",
  "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress": "vahid@dntips.ir",
  "Id": "582855fb-e95b-45ab-b349-5e9f7de40c0c",
  "DisplayName": "vahid@dntips.ir",
  "http://schemas.microsoft.com/ws/2008/06/identity/claims/role": "Admin",
  "nbf": 1616396383,
  "exp": 1616397583,
  "aud": "Any"
}
بنابراین برای استخراج این claims در سمت کلاینت، نیاز به یک JWT Parser داریم که نمونه‌ای از آن می‌تواند به صورت زیر باشد:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Text.Json;

namespace BlazorWasm.Client.Utils
{
    /// <summary>
    /// From the Steve Sanderson’s Mission Control project:
    /// https://github.com/SteveSandersonMS/presentation-2019-06-NDCOslo/blob/master/demos/MissionControl/MissionControl.Client/Util/ServiceExtensions.cs
    /// </summary>
    public static class JwtParser
    {
        public static IEnumerable<Claim> ParseClaimsFromJwt(string jwt)
        {
            var claims = new List<Claim>();
            var payload = jwt.Split('.')[1];

            var jsonBytes = ParseBase64WithoutPadding(payload);

            var keyValuePairs = JsonSerializer.Deserialize<Dictionary<string, object>>(jsonBytes);
            claims.AddRange(keyValuePairs.Select(kvp => new Claim(kvp.Key, kvp.Value.ToString())));
            return claims;
        }

        private static byte[] ParseBase64WithoutPadding(string base64)
        {
            switch (base64.Length % 4)
            {
                case 2: base64 += "=="; break;
                case 3: base64 += "="; break;
            }
            return Convert.FromBase64String(base64);
        }
    }
}
که آن‌را در فایل BlazorWasm.Client\Utils\JwtParser.cs برنامه‌ی کلاینت ذخیره خواهیم کرد. متد ParseClaimsFromJwt فوق، رشته‌ی JWT تولیدی حاصل از لاگین موفق در سمت Web API را دریافت کرده و تبدیل به لیستی از Claimها می‌کند.


تامین AuthenticationState مبتنی بر JWT مخصوص برنامه‌‌های Blazor WASM

پس از داشتن لیست Claims دریافتی از یک رشته‌ی JWT، اکنون می‌توان آن‌را تبدیل به یک AuthenticationStateProvider کرد. برای اینکار در ابتدا نیاز است بسته‌ی نیوگت Microsoft.AspNetCore.Components.Authorization را به برنامه‌ی کلاینت اضافه کرد:
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="5.0.4" />
  </ItemGroup>
</Project>
سپس سرویس سفارشی AuthStateProvider خود را به پوشه‌ی Services برنامه اضافه می‌کنیم و متد GetAuthenticationStateAsync کلاس پایه‌ی AuthenticationStateProvider استاندارد را به نحو زیر بازنویسی و سفارشی سازی می‌کنیم:
namespace BlazorWasm.Client.Services
{
    public class AuthStateProvider : AuthenticationStateProvider
    {
        private readonly HttpClient _httpClient;
        private readonly ILocalStorageService _localStorage;

        public AuthStateProvider(HttpClient httpClient, ILocalStorageService localStorage)
        {
            _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
            _localStorage = localStorage ?? throw new ArgumentNullException(nameof(localStorage));
        }

        public override async Task<AuthenticationState> GetAuthenticationStateAsync()
        {
            var token = await _localStorage.GetItemAsync<string>(ConstantKeys.LocalToken);
            if (token == null)
            {
                return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
            }

            _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", token);
            return new AuthenticationState(
                        new ClaimsPrincipal(
                            new ClaimsIdentity(JwtParser.ParseClaimsFromJwt(token), "jwtAuthType")
                        )
                    );
        }
    }
}
- اگر با برنامه‌های سمت کلاینت React و یا Angular پیشتر کار کرده باشید، منطق این کلاس بسیار آشنا به نظر می‌رسد. در این برنامه‌ها، مفهومی به نام Interceptor وجود دارد که توسط آن به صورت خودکار، هدر JWT را به تمام درخواست‌های ارسالی به سمت سرور، اضافه می‌کنند تا از تکرار این قطعه کد خاص، جلوگیری شود. علت اینجا است که برای دسترسی به منابع محافظت شده‌ی سمت سرور، نیاز است هدر ویژه‌ای را به نام "Authorization" که با مقدار "bearer jwt" تشکیل می‌شود، به ازای هر درخواست ارسالی به سمت سرور نیز ارسال کرد؛ تا تنظیمات ویژه‌ی AddJwtBearer که در قسمت 25 در کلاس آغازین برنامه‌ی Web API انجام دادیم، این هدر مورد انتظار را دریافت کرده و پردازش کند و در نتیجه‌ی آن، شیء this.User، در اکشن متدهای کنترلرها تشکیل شده و قابل استفاده شود.
در اینجا نیز مقدار دهی خودکار httpClient.DefaultRequestHeaders.Authorization را مشاهده می‌کنید که مقدار token خودش را از Local Storage دریافت می‌کند که کلید متناظر با آن‌را در پروژه‌ی BlazorServer.Common به صورت زیر تعریف کرده‌ایم:
namespace BlazorServer.Common
{
    public static class ConstantKeys
    {
        // ...
        public const string LocalToken = "JWT Token";
    }
}
به این ترتیب دیگر نیازی نخواهد بود در تمام سرویس‌های برنامه‌ی WASM که با HttpClient کار می‌کنند، مدام سطر مقدار دهی httpClient.DefaultRequestHeaders.Authorization را تکرار کنیم.
- همچنین در اینجا به کمک متد JwtParser.ParseClaimsFromJwt که در ابتدای بحث تهیه کردیم، لیست Claims دریافتی از JWT ارسالی از سمت سرور را تبدیل به یک AuthenticationState قابل استفاده‌ی در برنامه‌ی Blazor WASM کرده‌ایم.

پس از تعریف یک AuthenticationStateProvider سفارشی، باید آن‌را به همراه Authorization، به سیستم تزریق وابستگی‌های برنامه در فایل Program.cs اضافه کرد:
namespace BlazorWasm.Client
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            // ...

            builder.Services.AddAuthorizationCore();
            builder.Services.AddScoped<AuthenticationStateProvider, AuthStateProvider>();

            // ...
        }
    }
}
و برای سهولت استفاده‌ی از امکانات اعتبارسنجی فوق در کامپوننت‌های برنامه، فضای نام زیر را به فایل BlazorWasm.Client\_Imports.razor اضافه می‌کنیم:
@using Microsoft.AspNetCore.Components.Authorization


تهیه‌ی سرویسی برای کار با AccountController

اکنون می‌خواهیم در برنامه‌ی سمت کلاینت، از AccountController سمت سرور که آن‌را در قسمت 25 این سری تهیه کردیم، استفاده کنیم. بنابراین نیاز است سرویس زیر را تدارک دید که امکان لاگین، ثبت نام و خروج از سیستم را در سمت کلاینت میسر می‌کند:
namespace BlazorWasm.Client.Services
{
    public interface IClientAuthenticationService
    {
        Task<AuthenticationResponseDTO> LoginAsync(AuthenticationDTO userFromAuthentication);
        Task LogoutAsync();
        Task<RegisterationResponseDTO> RegisterUserAsync(UserRequestDTO userForRegisteration);
    }
}
و به صورت زیر پیاده سازی می‌شود:
namespace BlazorWasm.Client.Services
{
    public class ClientAuthenticationService : IClientAuthenticationService
    {
        private readonly HttpClient _client;
        private readonly ILocalStorageService _localStorage;

        public ClientAuthenticationService(HttpClient client, ILocalStorageService localStorage)
        {
            _client = client;
            _localStorage = localStorage;
        }

        public async Task<AuthenticationResponseDTO> LoginAsync(AuthenticationDTO userFromAuthentication)
        {
            var response = await _client.PostAsJsonAsync("api/account/signin", userFromAuthentication);
            var responseContent = await response.Content.ReadAsStringAsync();
            var result = JsonSerializer.Deserialize<AuthenticationResponseDTO>(responseContent);

            if (response.IsSuccessStatusCode)
            {
                await _localStorage.SetItemAsync(ConstantKeys.LocalToken, result.Token);
                await _localStorage.SetItemAsync(ConstantKeys.LocalUserDetails, result.UserDTO);
                _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", result.Token);
                return new AuthenticationResponseDTO { IsAuthSuccessful = true };
            }
            else
            {
                return result;
            }
        }

        public async Task LogoutAsync()
        {
            await _localStorage.RemoveItemAsync(ConstantKeys.LocalToken);
            await _localStorage.RemoveItemAsync(ConstantKeys.LocalUserDetails);
            _client.DefaultRequestHeaders.Authorization = null;
        }

        public async Task<RegisterationResponseDTO> RegisterUserAsync(UserRequestDTO userForRegisteration)
        {
            var response = await _client.PostAsJsonAsync("api/account/signup", userForRegisteration);
            var responseContent = await response.Content.ReadAsStringAsync();
            var result = JsonSerializer.Deserialize<RegisterationResponseDTO>(responseContent);

            if (response.IsSuccessStatusCode)
            {
                return new RegisterationResponseDTO { IsRegisterationSuccessful = true };
            }
            else
            {
                return result;
            }
        }
    }
}
که به نحو زیر به سیستم تزریق وابستگی‌های برنامه معرفی می‌شود:
namespace BlazorWasm.Client
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            // ...
            builder.Services.AddScoped<IClientAuthenticationService, ClientAuthenticationService>();
            // ...
        }
    }
}
توضیحات:
- متد LoginAsync، مشخصات لاگین کاربر را به سمت اکشن متد api/account/signin ارسال کرده و در صورت موفقیت این عملیات، اصل توکن دریافتی را به همراه مشخصاتی از کاربر، در Local Storage ذخیره سازی می‌کند. این مورد سبب خواهد شد تا بتوان به مشخصات کاربر در صفحات دیگر و سرویس‌های دیگری مانند AuthStateProvider ای که تهیه کردیم، دسترسی پیدا کنیم. به علاوه مزیت دیگر کار با Local Storage، مواجه شدن با حالت‌هایی مانند Refresh کامل صفحه و برنامه، توسط کاربر است. در یک چنین حالتی، برنامه از نو بارگذاری مجدد می‌شود و به این ترتیب می‌توان به مشخصات کاربر لاگین کرده، به سادگی دسترسی یافت و مجددا قسمت‌های مختلف برنامه را به او نشان داد. نمونه‌ی دیگر این سناریو، بازگشت از درگاه پرداخت بانکی است. در این حالت نیز از یک سرویس سمت سرور دیگر، کاربر به سمت برنامه‌ی کلاینت، Redirect کامل خواهد شد که در اصل اتفاقی که رخ می‌دهد، با Refresh کامل صفحه یکی است. در این حالت نیز باید بتوان کاربری را که از درگاه بانکی ثالث، به سمت برنامه‌ی کلاینت از نو بارگذاری شده، هدایت شده، بلافاصله تشخیص داد.

- اگر برنامه، Refresh کامل نشود، نیازی به Local Storage نخواهد بود؛ از این لحاظ که در برنامه‌های سمت کلاینت Blazor، طول عمر تمام سرویس‌ها، صرفنظر از نوع طول عمری که برای آن‌ها مشخص می‌کنیم، همواره Singleton هستند (ماخذ).
Blazor WebAssembly apps don't currently have a concept of DI scopes. Scoped-registered services behave like Singleton services.
بنابراین می‌توان یک سرویس سراسری توکن را تهیه و به سادگی آن‌را در تمام قسمت‌های برنامه تزریق کرد. این روش هرچند کار می‌کند، اما همانطور که عنوان شد، به Refresh کامل صفحه حساس است. اگر برنامه در مرورگر کاربر Refresh نشود، تا زمانیکه باز است، سرویس‌های در اصل Singleton تعریف شده‌ی در آن نیز در تمام قسمت‌های برنامه در دسترس هستند؛ اما با Refresh کامل صفحه، به علت بارگذاری مجدد کل برنامه، سرویس‌های آن نیز از نو، وهله سازی خواهند شد که سبب از دست رفتن حالت قبلی آن‌ها می‌شود. بنابراین نیاز به روشی داریم که بتوانیم حالت قبلی برنامه را در زمان راه اندازی اولیه‌ی آن بازیابی کنیم و یکی از روش‌های استاندارد اینکار، استفاده از Local Storage خود مرورگر است که مستقل از برنامه و توسط مرورگر مدیریت می‌شود.

- در متد LoginAsync، علاوه بر ثبت اطلاعات کاربر در Local Storage، مقدار دهی client.DefaultRequestHeaders.Authorization را نیز ملاحظه می‌کنید. همانطور که عنوان شد، سرویس‌های Blazor WASM در اصل دارای طول عمر Singleton هستند. بنابراین تنظیم این هدر در اینجا، بر روی تمام سرویس‌های HttpClient تزریق شده‌ی به سایر سرویس‌های برنامه نیز بلافاصله تاثیرگذار خواهد بود.

- متد LogoutAsync، اطلاعاتی را که در حین لاگین موفق در Local Storage ذخیره کردیم، حذف کرده و همچنین client.DefaultRequestHeaders.Authorization را نیز نال می‌کند تا دیگر اطلاعات لاگین شخص قابل بازیابی نبوده و مورد استفاده قرار نگیرد. همین مقدار برای شکست پردازش درخواست‌های ارسالی به منابع محافظت شده‌ی سمت سرور کفایت می‌کند.

- متد RegisterUserAsync، مشخصات کاربر در حال ثبت نام را به سمت اکشن متد api/account/signup ارسال می‌کند که سبب افزوده شدن کاربر جدیدی به بانک اطلاعاتی برنامه و سیستم ASP.NET Core Identity خواهد شد.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-31.zip
مطالب مشابه
  • #
    ‫۳ سال و ۵ ماه قبل، شنبه ۱۴ فروردین ۱۴۰۰، ساعت ۰۲:۴۹
    با سلام
    قسمت JWT Parser  رولهای کاربر را بصورت [Admin,User] استخراج میکنه و کاربر دارای دسترسی Admin , با بررسی متد user.IsInRole("Admin") مقدار false را بر میگرداند  و با اضافه کردن قسمت زیر مشکل برطرف میشود .
    public static IEnumerable<Claim> ParseClaimsFromJwt(string jwt)
            {
                var claims = new List<Claim>();
                var payload = jwt.Split('.')[1];
    
                var jsonBytes = ParseBase64WithoutPadding(payload);
    
                var keyValuePairs = JsonSerializer.Deserialize<Dictionary<string, object>>(jsonBytes);
               
                claims.AddRange(keyValuePairs.Select(kvp => new Claim(kvp.Key, kvp.Value.ToString())));
                ExtractRolesFromJwt(claims, keyValuePairs);
                return claims;
            }

    private static void ExtractRolesFromJwt(List<Claim> claims, Dictionary<string, object> keyValuePairs)
            {
                keyValuePairs.TryGetValue(ClaimTypes.Role, out object roles);
                if (roles != null)
                {
                    var parsedRoles = roles.ToString().Trim().TrimStart('[').TrimEnd(']').Split(',');
                    if (parsedRoles.Length > 1)
                    {
                        claims.AddRange(parsedRoles.Select(parsedRole => new Claim(ClaimTypes.Role, parsedRole.Trim('"'))));
                    }
                    else
                    {
                        claims.Add(new Claim(ClaimTypes.Role, parsedRoles[0]));
                    }
                    keyValuePairs.Remove(ClaimTypes.Role);
                }
            }

    • #
      ‫۳ سال و ۵ ماه قبل، شنبه ۱۴ فروردین ۱۴۰۰، ساعت ۰۳:۵۶
      بله. اگر کاربری برای مثال دو نقش داشته باشد، این دو نقش به صورت یک آرایه در JWT ظاهر می‌شوند:
      {
        "iss": "https://localhost:5001/",
        "iat": 1617386360,
        "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": "vahid@dntips.ir",
        "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress": "vahid@dntips.ir",
        "Id": "14fc329a-6198-4b0d-958d-daa9f07707ec",
        "DisplayName": "vahid@dntips.ir",
        "http://schemas.microsoft.com/ws/2008/06/identity/claims/role": [
          "Admin",
          "Employee"
        ],
        "nbf": 1617386360,
        "exp": 1617387560,
        "aud": "Any"
      }
      که این آرایه را به صورت زیر هم می‌توان پردازش کرد:
          public static class JwtParser
          {
              public static IEnumerable<Claim> ParseClaimsFromJwt(string jwt)
              {
                  var claims = new List<Claim>();
                  var payload = jwt.Split('.')[1];
      
                  var jsonBytes = ParseBase64WithoutPadding(payload);
      
                  var keyValuePairs = JsonSerializer.Deserialize<Dictionary<string, object>>(jsonBytes);
                  foreach(var item in keyValuePairs)
                  {
                      if (item.Value is JsonElement element && element.ValueKind == JsonValueKind.Array)
                      {
                         foreach(var itemValue in element.EnumerateArray())
                         {
                            claims.Add(new Claim(item.Key, itemValue.ToString()));
                         } 
                      }
                      else
                      {
                         claims.Add(new Claim(item.Key, item.Value.ToString()));
                      }
                  }
                  return claims;
              }
  • #
    ‫۳ سال و ۵ ماه قبل، شنبه ۱۴ فروردین ۱۴۰۰، ساعت ۱۶:۱۷
    یک نکته‌ی تکمیلی: همیشه بهتر است در سمت کلاینت هم تاریخ انقضای JWT پیش از استفاده بررسی شود
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Security.Claims;
    using System.Text.Json;
    
    namespace BlazorWasm.Client.Utils
    {
        public class JwtInfo
        {
            public IEnumerable<Claim> Claims { set; get; }
    
            public DateTime? ExpirationDateUtc { set; get; }
    
            public bool IsExpired { set; get; }
    
            public IEnumerable<string> Roles { set; get; }
        }
    
        /// <summary>
        /// From the Steve Sanderson’s Mission Control project:
        /// https://github.com/SteveSandersonMS/presentation-2019-06-NDCOslo/blob/master/demos/MissionControl/MissionControl.Client/Util/ServiceExtensions.cs
        /// </summary>
        public static class JwtParser
        {
            public static JwtInfo ParseClaimsFromJwt(string jwt)
            {
                var claims = new List<Claim>();
                var payload = jwt.Split('.')[1];
    
                var jsonBytes = getBase64WithoutPadding(payload);
    
                foreach (var keyValue in JsonSerializer.Deserialize<Dictionary<string, object>>(jsonBytes))
                {
                    if (keyValue.Value is JsonElement element && element.ValueKind == JsonValueKind.Array)
                    {
                        foreach (var itemValue in element.EnumerateArray())
                        {
                            claims.Add(new Claim(keyValue.Key, itemValue.ToString()));
                        }
                    }
                    else
                    {
                        claims.Add(new Claim(keyValue.Key, keyValue.Value.ToString()));
                    }
                }
    
                var roles = getRoles(claims);
                var expirationDateUtc = getDateUtc(claims, "exp");
                var isExpired = getIsExpired(expirationDateUtc);
                return new JwtInfo
                {
                    Claims = claims,
                    Roles = roles,
                    ExpirationDateUtc = expirationDateUtc,
                    IsExpired = isExpired
                };
            }
    
            private static IList<string> getRoles(IList<Claim> claims) =>
                claims.Where(c => c.Type == ClaimTypes.Role).Select(c => c.Value).ToList();
    
            private static byte[] getBase64WithoutPadding(string base64)
            {
                switch (base64.Length % 4)
                {
                    case 2: base64 += "=="; break;
                    case 3: base64 += "="; break;
                }
                return Convert.FromBase64String(base64);
            }
    
            private static bool getIsExpired(DateTime? expirationDateUtc) =>
                !expirationDateUtc.HasValue || !(expirationDateUtc.Value > DateTime.UtcNow);
    
            private static DateTime? getDateUtc(IList<Claim> claims, string type)
            {
                var exp = claims.SingleOrDefault(claim => claim.Type == type);
                if (exp == null)
                {
                    return null;
                }
    
                var expValue = getTimeValue(exp.Value);
                if (expValue == null)
                {
                    return null;
                }
    
                var dateTimeEpoch = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
                return dateTimeEpoch.AddSeconds(expValue.Value);
            }
    
            private static long? getTimeValue(string claimValue)
            {
                if (long.TryParse(claimValue, out long resultLong))
                    return resultLong;
    
                if (float.TryParse(claimValue, out float resultFloat))
                    return (long)resultFloat;
    
                if (double.TryParse(claimValue, out double resultDouble))
                    return (long)resultDouble;
    
                return null;
            }
        }
    }
    • #
      ‫۳ سال و ۳ ماه قبل، دوشنبه ۱۰ خرداد ۱۴۰۰، ساعت ۰۱:۲۸
      در صورت استفاده از این شیوه مقدار بازگشتی از نوع JwtInfo در متد ParseClaimsFromJwt  قابل تبدیل به نوع IEnumerable<Claim> در متد GetAuthenticationStateAsync در سرویس AuthStateProvider  نیست و در همین مورد پیام خطای عدم امکان تبدیل نوع وجود دارد. تنها پیشنهاد resharper تبدیل صریح به (IEnumerable<Claim>) هست که همان هم همراه با هشدار suspicious cast است.

      چطور باید از مقدار بازگشتی JwtInfo  در سرویس AuthStateProvider استفاده کرد؟

      // In ParseClaimsFromJwt method
       return new JwtInfo
        {
           Claims = claims,
           Roles = roles,
           ExpirationDateUtc = expirationDateUtc,
           IsExpired = isExpired
        };
      
      // In GetAuthenticationStateAsync with cannot convert error
       return new AuthenticationState(
                      new ClaimsPrincipal(
                          new ClaimsIdentity(JwtParser.ParseClaimsFromJwt(token), "jwtAuthType")
                      )
                  );
      
      // Resharper suggestion: Explicit cast but with suspicious cast warning.
        return new AuthenticationState(
                      new ClaimsPrincipal(
                          new ClaimsIdentity((IEnumerable<Claim>)JwtParser.ParseClaimsFromJwt(token), "jwtAuthType")
                      )
                  );

      ممنون. 
      • #
        ‫۳ سال و ۳ ماه قبل، دوشنبه ۱۰ خرداد ۱۴۰۰، ساعت ۰۲:۱۷
        از خاصیت jwtInfo.Claims استفاده کنید.
  • #
    ‫۳ سال و ۳ ماه قبل، سه‌شنبه ۱۱ خرداد ۱۴۰۰، ساعت ۰۱:۴۵
    سلام و تشکر از شما
    برای نمایش نام مستعار کاربر یا DisplayName که فارسی و ترکیبی از نام و نام خانوادگی است,  DisplayName  را به Claims‌ها اضافه میکنم ولی هنگام لاگین جهت بعضی کاربران خطایی صادر میشود: مثلا کاربر A و نام مستعار محمد استواری
    blazor.webassembly.js:1 crit: Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer[100]
          Unhandled exception rendering component: The input is not a valid Base-64 string as it contains a non-base 64 character, more than two padding characters, or an illegal character among the padding characters.
    System.FormatException: The input is not a valid Base-64 string as it contains a non-base 64 character, more than two padding characters, or an illegal character among the padding characters.
       at System.Convert.FromBase64CharPtr(Char* inputPtr, Int32 inputLength)
       at System.Convert.FromBase64String(String s)
     نکته اینجاست که وقتی پروژه را در سیستم‌های دیگراجرا میکنم لاگین با کاربر A  خطا صادر نمی‌کند و یا احتمال صدور خطا کمتر است ولی با بعضی کاربران دیگر خطا صادر میشود
    گویا مشکل با کاراکترهای فارسی هست و با نام مستعار لاتین هیچگونه خطایی صادر نمیشود.
    • #
      ‫۳ سال و ۳ ماه قبل، سه‌شنبه ۱۱ خرداد ۱۴۰۰، ساعت ۱۳:۳۹
      نگارش کامل و صحیح getBase64WithoutPadding به این صورت هست:
              private static byte[] getBase64WithoutPadding(string base64)
              {
                  // From:
                  // https://github.com/dvsekhvalnov/jose-jwt/blob/master/jose-jwt/util/Base64Url.cs#L16
                  // https://github.com/auth0/jwt-decode/blob/master/lib/base64_url_decode.js#L15
                  // https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/blob/0665af62cc58a28ebe184dd97f4d018f84e1d83d/src/Microsoft.IdentityModel.Tokens/Base64UrlEncoder.cs#L175
      
                  base64 = base64.Replace('-', '+'); // 62nd char of encoding
                  base64 = base64.Replace('_', '/'); // 63rd char of encoding
                  switch (base64.Length % 4) // Pad with trailing '='s
                  {
                      case 0: break; // No pad chars in this case
                      case 2: base64 += "=="; break; // Two pad chars
                      case 3: base64 += "="; break; // One pad char
                      default: throw new ArgumentOutOfRangeException(nameof(base64), "Illegal base64url string!");
                  }
                  return Convert.FromBase64String(base64); // Standard base64 decoder
              }
  • #
    ‫۲ سال و ۸ ماه قبل، چهارشنبه ۱ دی ۱۴۰۰، ساعت ۰۷:۴۵
    ضمن قدردانی از انتشار این دوره، در یک پروژه‌ی Blazor Server با در نظر گرفتن اینکه :

    - از Asp.net Identity استفاده نمیکنیم.
    - دسترسی به دیتا فقط از طریق API بوده و API‌ها هم نیاز به احراز هویت از طریق JWT دارند.
    - از cookie authentication برای احراز هویت در سمت اپلیکیشن Blazor Server استفاده شده. 
    موارد زیر باید چگونه هندل شوند؟

    - توکن JWT که از API لاگین دریافت کرده ایم بهتر است کجا ذخیره شود؟ با توجه به اینکه در سمت سرور دسترسی به Local Storage مرورگر نداریم.
    - چگونه میتوان DefaultRequestHeaders.Authorization را برای ارسال توکن jwt به نحوی مقدار دهی کرد که نیاز به تکرار آن در تمام فراخوانی‌های HttpClient نباشد؟
    - آیا امکان پیاده سازی jwt authentication در Blazor Server هم وجود دارد؟
    لینک سورس پروژه  بر روی گیت هاب.
    • #
      ‫۲ سال و ۸ ماه قبل، چهارشنبه ۱ دی ۱۴۰۰، ساعت ۱۰:۴۰
      - فلسفه‌ی کار با Blazor server، امکان دسترسی مستقیم به لایه سرویس‌های برنامه، بدون نیاز به طراحی یک Web API خاص آن‌ها است (کار با آن ساده‌تر است). اگر قرار است با Web API کار کنید، شاید بهتر باشد از WASM استفاده کنید.
      - سرویس‌های Blazor Server، طول عمر خاصی دارند و تا زمانیکه اتصال برقرار است، از دست نمی‌روند. بنابراین خیلی از اطلاعات را می‌توان به صورت متداولی در آن‌ها ذخیره کرد و نیازی به تمهید خاصی نیست؛ هرچند encrypted local storage هم دارند.
      - امکان طراحی interceptor برای HTTP Client هم وجود دارد تا هربار نیازی به مقدار دهی هدر Authorization نباشد.
      - بله. در این سری اگر از Identity استفاده شده، بیشتر هدف مدیریت کاربران بوده و یا برای Blazor Server، دسترسی به کوکی خودکار پس از لاگین. نکات تهیه‌ی authentication provider سفارشی مطرح شده‌ی در قسمت wasm این سری برای کار با JWT، همه جا یکسان است و وابستگی به Identity و یا حتی Identity server پیش‌فرض مطرح شده‌ی توسط مایکروسافت، ندارد.
  • #
    ‫۲ سال و ۲ ماه قبل، چهارشنبه ۸ تیر ۱۴۰۱، ساعت ۲۲:۵۷
    آیا برای امنیت بیشتر می‌شود access token و یا refresh token را در کوکی ذخیره کرد؟ برای استفاده از حالت  HttpOnly
    و اگر این کار صحیحی هست راهی به جز  JSIntop دارد؟
    ذخیره رفرش توکن و access token در local storage امنیت آنها را پایین نمی‌آورد؟
  • #
    ‫۲ سال قبل، پنجشنبه ۱۰ شهریور ۱۴۰۱، ساعت ۱۵:۴۴
    سلام؛ در صورتی که توکن را رمزگذاری کنیم مطابق آنچه در این پست به آن اشاره شده ، سمت blazor در JwtParser برای تامین AuthenticationState  چگونه آن را  رمز گشایی کنیم؟ 
    • #
      ‫۲ سال قبل، پنجشنبه ۱۰ شهریور ۱۴۰۱، ساعت ۱۶:۴۱
      نمی‌توانید. در این حالت باید پس از لاگین موفق، اطلاعات مورد نیاز را (قسمتی از همان اطلاعاتی را که داخل توکن قرار دادید) از یک endpoint محافظت شده به صورت جداگانه دریافت کنید و یا حتی می‌توانید این اطلاعات را جزئی از خروجی Response، پس از لاگین موفق قرار دهید.
      • #
        ‫۲ سال قبل، جمعه ۱۱ شهریور ۱۴۰۱، ساعت ۱۴:۱۱
        ممنون از پاسخ شما 
        ۱.در این حالت آیا پس از رفرش صفحه اطلاعات از بین نمی‌رود؟
        ۲.آیا می‌شود این اطلاعات مورد نیاز را در یک توکن دیگر ذخیره کرد . توکنی که access token نباشد فقط برای استفاده در authorovider
        ۳ . آیا اصلا نیازی به رمزگذاری توکن اصلی هست . با توجه به اینکه id کاربر هم در ان به صورت guid ذخیره شده؟
        • #
          ‫۲ سال قبل، جمعه ۱۱ شهریور ۱۴۰۱، ساعت ۱۵:۲۰
          - از بین می‌رود. باید آن‌را در local storage ذخیره کنید.
          - بله. نمونه اینکار در پروژه‌ی «پیاده سازی سیاست‌های دسترسی پویای سمت سرور و کلاینت در برنامه‌های Blazor WASM» پس از لاگین انجام شده.
          - خیر؛ نیازی نیست. توکن‌ها به همراه امضای دیجیتال هستند و همچنین کلید رمزنگاری خصوصی آن بر روی سرور ذخیره می‌شود. کسی که توانسته در سمت کلاینت توکنی را تغییر دهد و آن‌را از مرحله‌ی اعتبارسنجی خودکار سمت سرور رد کند (یعنی امضای دیجیتال تغییرات انجام شده را مجددا محاسبه و به توکن اعمال کرده)، به سرور شما و کلیدهای رمزنگاری خصوصی آن دسترسی دارد؛ یک چنین شخصی نیازی به دستکاری توکنی را ندارد، چون هم اکنون دسترسی او به سرور شما کامل است! محتوای یک JWT، هرچند قابل مشاهده و مطالعه است، اما امضاء شده‌است. یک چنین محتوای امضاء شده‌ای، در جاهای حساس دیگری هم کاربرد واقعی دارد: «تهیه XML امضاء شده جهت تولید مجوز استفاده از برنامه»