اعتبارسنجی مبتنی بر JWT در ASP.NET Core 2.0 بدون استفاده از سیستم Identity
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: پانزده دقیقه

AuthenticationMiddleware در ASP.NET Core 2.0، فقط مختص به کار با کوکی‌ها جهت اعتبارسنجی کاربران نیست. از این میان‌افزار می‌توان برای اعتبار سنجی‌های مبتنی بر JSON Web Tokens نیز استفاده کرد. مطلبی را که در ادامه مطالعه خواهید کرد دقیقا بر اساس نکات مطلب «پیاده سازی JSON Web Token با ASP.NET Web API 2.x» تدارک دیده شده‌است و به همراه نکاتی مانند تولید Refresh Tokens و یا غیرمعتبر سازی توکن‌ها نیز هست. همچنین ساختار جداول کاربران و نقش‌های آن‌ها، سرویس‌های مرتبط و قسمت تنظیمات Context آن با مطلب «اعتبارسنجی مبتنی بر کوکی‌ها در ASP.NET Core 2.0 بدون استفاده از سیستم Identity» یکی است. در اینجا بیشتر به تفاوت‌های پیاده سازی این روش نسبت به حالت اعتبارسنجی مبتنی بر کوکی‌ها خواهیم پرداخت.
همچنین باید درنظر داشت، ASP.NET Core Identity یک سیستم اعتبارسنجی مبتنی بر کوکی‌ها است. دقیقا زمانیکه کار AddIdentity را انجام می‌دهیم، در پشت صحنه همان  services.AddAuthentication().AddCookie قسمت قبل فراخوانی می‌شود. بنابراین بکارگیری آن با JSON Web Tokens هرچند مشکلی را به همراه ندارد و می‌توان یک سیستم اعتبارسنجی «دوگانه» را نیز در اینجا داشت، اما ... سربار اضافی تولید کوکی‌ها را نیز به همراه دارد؛ هرچند برای کار با میان‌افزار اعتبارسنجی، الزامی به استفاده‌ی از ASP.NET Core Identity نیست و عموما اگر از آن به همراه JWT استفاده می‌کنند، بیشتر به دنبال پیاده سازی‌های پیش‌فرض مدیریت کاربران و نقش‌های آن هستند و نه قسمت تولید کوکی‌های آن. البته در مطلب جاری این موارد را نیز همانند مطلب اعتبارسنجی مبتنی بر کوکی‌ها، خودمان مدیریت خواهیم کرد و در نهایت سیستم تهیه شده، هیچ نوع کوکی را تولید و یا مدیریت نمی‌کند.



تنظیمات آغازین برنامه جهت فعالسازی اعتبارسنجی مبتنی بر JSON Web Tokens

اولین تفاوت پیاده سازی یک سیستم اعتبارسنجی مبتنی بر JWT، با روش مبتنی بر کوکی‌ها، تنظیمات متد ConfigureServices فایل آغازین برنامه است:
        public void ConfigureServices(IServiceCollection services)
        {
            services.Configure<BearerTokensOptions>(options => Configuration.GetSection("BearerTokens").Bind(options));

            services
                .AddAuthentication(options =>
                {
                    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
                    options.DefaultSignInScheme = JwtBearerDefaults.AuthenticationScheme;
                    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                })
                .AddJwtBearer(cfg =>
                {
                    cfg.RequireHttpsMetadata = false;
                    cfg.SaveToken = true;
                    cfg.TokenValidationParameters = new TokenValidationParameters
                    {
                        ValidIssuer = Configuration["BearerTokens:Issuer"],
                        ValidAudience = Configuration["BearerTokens:Audience"],
                        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["BearerTokens:Key"])),
                        ValidateIssuerSigningKey = true,
                        ValidateLifetime = true,
                        ClockSkew = TimeSpan.Zero
                    };
                    cfg.Events = new JwtBearerEvents
                    {
                        OnAuthenticationFailed = context =>
                        {
                            var logger = context.HttpContext.RequestServices.GetRequiredService<ILoggerFactory>().CreateLogger(nameof(JwtBearerEvents));
                            logger.LogError("Authentication failed.", context.Exception);
                            return Task.CompletedTask;
                        },
                        OnTokenValidated = context =>
                        {
                            var tokenValidatorService = context.HttpContext.RequestServices.GetRequiredService<ITokenValidatorService>();
                            return tokenValidatorService.ValidateAsync(context);
                        },
                        OnMessageReceived = context =>
                         {
                             return Task.CompletedTask;
                         },
                        OnChallenge = context =>
                        {
                            var logger = context.HttpContext.RequestServices.GetRequiredService<ILoggerFactory>().CreateLogger(nameof(JwtBearerEvents));
                            logger.LogError("OnChallenge error", context.Error, context.ErrorDescription);
                            return Task.CompletedTask;
                        }
                    };
                });
در اینجا در ابتدا تنظیمات JWT فایل appsettings.json
{
  "BearerTokens": {
    "Key": "This is my shared key, not so secret, secret!",
    "Issuer": "http://localhost/",
    "Audience": "Any",
    "AccessTokenExpirationMinutes": 2,
    "RefreshTokenExpirationMinutes": 60
  }
}
به کلاسی دقیقا با همین ساختار به نام BearerTokensOptions، نگاشت شده‌اند. به این ترتیب می‌توان با تزریق اینترفیس <IOptionsSnapshot<BearerTokensOptions در قسمت‌های مختلف برنامه، به این تنظیمات مانند کلید رمزنگاری، مشخصات صادر کننده، مخاطبین و طول عمرهای توکن‌های صادر شده، دسترسی یافت.

سپس کار فراخوانی  services.AddAuthentication صورت گرفته‌است. تفاوت این مورد با حالت اعتبارسنجی مبتنی بر کوکی‌ها، ثوابتی است که با JwtBearerDefaults شروع می‌شوند. در حالت استفاده‌ی از کوکی‌ها، این ثوابت بر اساس CookieAuthenticationDefaults تنظیم خواهند شد.
البته می‌توان متد AddAuthentication را بدون هیچگونه پارامتری نیز فراخوانی کرد. این حالت برای اعتبارسنجی‌های دوگانه مفید است. برای مثال زمانیکه پس از AddAuthentication هم AddJwtBearer را ذکر کرده‌اید و هم AddCookie اضافه شده‌است. اگر چنین کاری را انجام دادید، اینبار باید درحین تعریف فیلتر Authorize، دقیقا مشخص کنید که حالت مبتنی بر JWT مدنظر شما است، یا حالت مبتنی بر کوکی‌ها:
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
اگر متد AddAuthentication، مانند تنظیمات فوق به همراه این تنظیمات پیش‌فرض بود، دیگر نیازی به ذکر صریح AuthenticationSchemes در فیلتر Authorize نخواهد بود.


بررسی تنظیمات متد AddJwtBearer

در کدهای فوق، تنظیمات متد AddJwtBearer یک چنین مفاهیمی را به همراه دارند:
- تنظیم SaveToken به true، به این معنا است که می‌توان به توکن دریافتی از سمت کاربر، توسط متد HttpContext.GetTokenAsync در کنترلرهای برنامه دسترسی یافت.
در قسمت تنظیمات TokenValidationParameters آن:
- کار خواندن فایل appsettings.json برنامه جهت تنظیم صادر کننده و مخاطبین توکن انجام می‌شود. سپس IssuerSigningKey به یک کلید رمزنگاری متقارن تنظیم خواهد شد. این کلید نیز در تنظیمات برنامه قید می‌شود.
- تنظیم ValidateIssuerSigningKey به true سبب خواهد شد تا میان‌افزار اعتبارسنجی، بررسی کند که آیا توکن دریافتی از سمت کاربر توسط برنامه‌ی ما امضاء شده‌است یا خیر؟
- تنظیم ValidateLifetime به معنای بررسی خودکار طول عمر توکن دریافتی از سمت کاربر است. اگر توکن منقضی شده باشد، اعتبارسنجی به صورت خودکار خاتمه خواهد یافت.
- ClockSkew به معنای تنظیم یک تلرانس و حد تحمل مدت زمان منقضی شدن توکن در حالت ValidateLifetime است. در اینجا به صفر تنظیم شده‌است.

سپس به قسمت JwtBearerEvents می‌رسیم:
- OnAuthenticationFailed زمانی فراخوانی می‌شود که اعتبارسنج‌های تنظیمی فوق، با شکست مواجه شوند. برای مثال طول عمر توکن منقضی شده باشد و یا توسط ما امضاء نشده‌باشد. در اینجا می‌توان به این خطاها دسترسی یافت و درصورت نیاز آن‌ها را لاگ کرد.
- OnChallenge نیز یک سری دیگر از خطاهای اعتبارسنجی را پیش از ارسال آن‌ها به فراخوان در اختیار ما قرار می‌دهد.
- OnMessageReceived برای حالتی است که توکن دریافتی، توسط هدر مخصوص Bearer به سمت سرور ارسال نمی‌شود. عموما هدر ارسالی به سمت سرور یک چنین شکلی را دارد:
$.ajax({
     headers: { 'Authorization': 'Bearer ' + jwtToken },
اما اگر توکن شما به این شکل استاندارد دریافت نمی‌شود، می‌توان در رخ‌داد OnMessageReceived به اطلاعات درخواست جاری دسترسی یافت، توکن را از آن استخراج کرد و سپس آن‌را به خاصیت context.Token انتساب داد، تا به عنوان توکن اصلی مورد استفاده قرار گیرد. برای مثال:
const string tokenKey = "my.custom.jwt.token.key";
if (context.HttpContext.Items.ContainsKey(tokenKey))
{
    context.Token = (string)context.HttpContext.Items[tokenKey];
}
 - OnTokenValidated پس از کامل شدن اعتبارسنجی توکن دریافتی از سمت کاربر فراخوانی می‌شود. در اینجا اگر متد context.Fail را فراخوانی کنیم، این توکن، به عنوان یک توکن غیرمعتبر علامتگذاری می‌شود و عملیات اعتبارسنجی با شکست خاتمه خواهد یافت. بنابراین می‌توان از آن دقیقا مانند CookieValidatorService قسمت قبل که جهت واکنش نشان دادن به تغییرات اطلاعات کاربر در سمت سرور مورد استفاده قرار دادیم، در اینجا نیز یک چنین منطقی را پیاده سازی کنیم.


تهیه یک اعتبارسنج توکن سفارشی

قسمت OnTokenValidated تنظیمات ابتدای برنامه به این صورت مقدار دهی شده‌است:
OnTokenValidated = context =>
{
      var tokenValidatorService = context.HttpContext.RequestServices.GetRequiredService<ITokenValidatorService>();
      return tokenValidatorService.ValidateAsync(context);
},
TokenValidatorService سفارشی ما چنین پیاده سازی را دارد:
    public class TokenValidatorService : ITokenValidatorService
    {
        private readonly IUsersService _usersService;
        private readonly ITokenStoreService _tokenStoreService;

        public TokenValidatorService(IUsersService usersService, ITokenStoreService tokenStoreService)
        {
            _usersService = usersService;
            _usersService.CheckArgumentIsNull(nameof(usersService));

            _tokenStoreService = tokenStoreService;
            _tokenStoreService.CheckArgumentIsNull(nameof(_tokenStoreService));
        }

        public async Task ValidateAsync(TokenValidatedContext context)
        {
            var userPrincipal = context.Principal;

            var claimsIdentity = context.Principal.Identity as ClaimsIdentity;
            if (claimsIdentity?.Claims == null || !claimsIdentity.Claims.Any())
            {
                context.Fail("This is not our issued token. It has no claims.");
                return;
            }

            var serialNumberClaim = claimsIdentity.FindFirst(ClaimTypes.SerialNumber);
            if (serialNumberClaim == null)
            {
                context.Fail("This is not our issued token. It has no serial.");
                return;
            }

            var userIdString = claimsIdentity.FindFirst(ClaimTypes.UserData).Value;
            if (!int.TryParse(userIdString, out int userId))
            {
                context.Fail("This is not our issued token. It has no user-id.");
                return;
            }

            var user = await _usersService.FindUserAsync(userId).ConfigureAwait(false);
            if (user == null || user.SerialNumber != serialNumberClaim.Value || !user.IsActive)
            {
                // user has changed his/her password/roles/stat/IsActive
                context.Fail("This token is expired. Please login again.");
            }

            var accessToken = context.SecurityToken as JwtSecurityToken;
            if (accessToken == null || string.IsNullOrWhiteSpace(accessToken.RawData) ||
                !await _tokenStoreService.IsValidTokenAsync(accessToken.RawData, userId).ConfigureAwait(false))
            {
                context.Fail("This token is not in our database.");
                return;
            }

            await _usersService.UpdateUserLastActivityDateAsync(userId).ConfigureAwait(false);
        }
    }
در اینجا بررسی می‌کنیم:
- آیا توکن دریافتی به همراه Claims تنظیم شده‌ی درحین لاگین هست یا خیر؟
- آیا توکن دریافتی دارای یک Claim سفارشی به نام SerialNumber است؟ این SerialNumber معادل چنین فیلدی در جدول کاربران است.
- آیا توکن دریافتی دارای user-id است؟
- آیا کاربر یافت شده‌ی بر اساس این user-id هنوز فعال است و یا اطلاعات او تغییر نکرده‌است؟
- همچنین در آخر کار بررسی می‌کنیم که آیا اصل توکن دریافتی، در بانک اطلاعاتی ما پیشتر ثبت شده‌است یا خیر؟

اگر خیر، بلافاصله متد context.Fail فراخوانی شده و کار اعتبارسنجی را با اعلام شکست آن، به پایان می‌رسانیم.

در قسمت آخر، نیاز است اطلاعات توکن‌های صادر شده را ذخیره کنیم. به همین جهت نسبت به مطلب قبلی، جدول UserToken ذیل به برنامه اضافه شده‌است:
    public class UserToken
    {
        public int Id { get; set; }

        public string AccessTokenHash { get; set; }

        public DateTimeOffset AccessTokenExpiresDateTime { get; set; }

        public string RefreshTokenIdHash { get; set; }

        public DateTimeOffset RefreshTokenExpiresDateTime { get; set; }

        public int UserId { get; set; } // one-to-one association
        public virtual User User { get; set; }
    }
در اینجا هش‌های توکن‌های صادر شده‌ی توسط برنامه و طول عمر آن‌ها را ذخیره خواهیم کرد.
از اطلاعات آن در دو قسمت TokenValidatorService فوق و همچنین قسمت logout برنامه استفاده می‌کنیم. در سیستم JWT، مفهوم logout سمت سرور وجود خارجی ندارد. اما با ذخیره سازی هش توکن‌ها در بانک اطلاعاتی می‌توان لیستی از توکن‌های صادر شده‌ی توسط برنامه را تدارک دید. سپس در حین logout فقط کافی است tokenهای یک کاربر را حذف کرد. همینقدر سبب خواهد شد تا قسمت آخر TokenValidatorService با شکست مواجه شود؛ چون توکن ارسالی به سمت سرور دیگر در بانک اطلاعاتی وجود ندارد.


سرویس TokenStore

    public interface ITokenStoreService
    {
        Task AddUserTokenAsync(UserToken userToken);
        Task AddUserTokenAsync(
                User user, string refreshToken, string accessToken,
                DateTimeOffset refreshTokenExpiresDateTime, DateTimeOffset accessTokenExpiresDateTime);
        Task<bool> IsValidTokenAsync(string accessToken, int userId);
        Task DeleteExpiredTokensAsync();
        Task<UserToken> FindTokenAsync(string refreshToken);
        Task DeleteTokenAsync(string refreshToken);
        Task InvalidateUserTokensAsync(int userId);
        Task<(string accessToken, string refreshToken)> CreateJwtTokens(User user);
    }
در قسمت آخر اعتبارسنج سفارشی توکن، بررسی وجود توکن دریافتی، توسط سرویس TokenStore فوق صورت می‌گیرد. از این سرویس برای تولید، ذخیره سازی و حذف توکن‌ها استفاده خواهیم کرد.
پیاده سازی کامل این سرویس را در اینجا می‌توانید مشاهده کنید.


تولید Access Tokens و Refresh Tokens

پس از تنظیمات ابتدایی برنامه، اکنون می‌توانیم دو نوع توکن را تولید کنیم:

تولید Access Tokens
        private async Task<string> createAccessTokenAsync(User user, DateTime expires)
        {
            var claims = new List<Claim>
            {
                // Unique Id for all Jwt tokes
                new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
                // Issuer
                new Claim(JwtRegisteredClaimNames.Iss, _configuration.Value.Issuer),
                // Issued at
                new Claim(JwtRegisteredClaimNames.Iat, DateTime.UtcNow.ToUnixEpochDate().ToString(), ClaimValueTypes.Integer64),
                new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
                new Claim(ClaimTypes.Name, user.Username),
                new Claim("DisplayName", user.DisplayName),
                // to invalidate the cookie
                new Claim(ClaimTypes.SerialNumber, user.SerialNumber),
                // custom data
                new Claim(ClaimTypes.UserData, user.Id.ToString())
            };

            // add roles
            var roles = await _rolesService.FindUserRolesAsync(user.Id).ConfigureAwait(false);
            foreach (var role in roles)
            {
                claims.Add(new Claim(ClaimTypes.Role, role.Name));
            }

            var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration.Value.Key));
            var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
            var token = new JwtSecurityToken(
                issuer: _configuration.Value.Issuer,
                audience: _configuration.Value.Audience,
                claims: claims,
                notBefore: DateTime.UtcNow,
                expires: expires,
                signingCredentials: creds);
            return new JwtSecurityTokenHandler().WriteToken(token);
        }
این امکانات در اسمبلی زیر قرار دارند:
<ItemGroup>
   <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="2.0.0" />
</ItemGroup>
در اینجا ابتدا همانند کار با سیستم اعتبارسنجی مبتنی بر کوکی‌ها، نیاز است یک سری Claim تهیه شوند. به همین جهت SerialNumber، UserId و همچنین نقش‌های کاربر لاگین شده‌ی به سیستم را در اینجا به مجموعه‌ی Claims اضافه می‌کنیم. وجود این Claims است که سبب می‌شود فیلتر Authorize بتواند نقش‌ها را تشخیص داده و یا کاربر را اعتبارسنجی کند.
پس از تهیه‌ی Claims، اینبار بجای یک کوکی، یک JSON Web Toekn را توسط متد new JwtSecurityTokenHandler().WriteToken تهیه خواهیم کرد. این توکن حاوی Claims، به همراه اطلاعات طول عمر و امضای مرتبطی است.
حاصل آن نیز یک رشته‌است که دقیقا به همین فرمت به سمت کلاینت ارسال خواهد شد. البته ما در اینجا دو نوع توکن را به سمت کلاینت ارسال می‌کنیم:
        public async Task<(string accessToken, string refreshToken)> CreateJwtTokens(User user)
        {
            var now = DateTimeOffset.UtcNow;
            var accessTokenExpiresDateTime = now.AddMinutes(_configuration.Value.AccessTokenExpirationMinutes);
            var refreshTokenExpiresDateTime = now.AddMinutes(_configuration.Value.RefreshTokenExpirationMinutes);
            var accessToken = await createAccessTokenAsync(user, accessTokenExpiresDateTime.UtcDateTime).ConfigureAwait(false);
            var refreshToken = Guid.NewGuid().ToString().Replace("-", "");

            await AddUserTokenAsync(user, refreshToken, accessToken, refreshTokenExpiresDateTime, accessTokenExpiresDateTime).ConfigureAwait(false);
            await _uow.SaveChangesAsync().ConfigureAwait(false);

            return (accessToken, refreshToken);
        }
accessToken همان JSON Web Token اصلی است. refreshToken فقط یک Guid است. کار آن ساده سازی و به روز رسانی عملیات Login بدون ارائه‌ی نام کاربری و کلمه‌ی عبور است. به همین جهت است که نیاز داریم تا این اطلاعات را در سمت بانک اطلاعاتی برنامه نیز ذخیره کنیم. فرآیند اعتبارسنجی یک refreshToken بدون ذخیره سازی این Guid در بانک اطلاعاتی مسیر نیست که در اینجا در فیلد RefreshTokenIdHash جدول UserToken ذخیره می‌شود.
جهت بالا رفتن امنیت سیستم، این Guid را هش کرد و سپس این هش را در بانک اطلاعاتی ذخیره می‌کنیم. به این ترتیب دسترسی غیرمجاز به این هش‌ها، امکان بازیابی توکن‌های اصلی را غیرممکن می‌کند.


پیاده سازی Login

پس از پیاده سازی متد CreateJwtTokens، کار ورود به سیستم به سادگی ذیل خواهد بود:
        [AllowAnonymous]
        [HttpPost("[action]")]
        public async Task<IActionResult> Login([FromBody]  User loginUser)
        {
            if (loginUser == null)
            {
                return BadRequest("user is not set.");
            }

            var user = await _usersService.FindUserAsync(loginUser.Username, loginUser.Password).ConfigureAwait(false);
            if (user == null || !user.IsActive)
            {
                return Unauthorized();
            }

            var (accessToken, refreshToken) = await _tokenStoreService.CreateJwtTokens(user).ConfigureAwait(false);
            return Ok(new { access_token = accessToken, refresh_token = refreshToken });
        }
ابتدا بررسی می‌شود که آیا کلمه‌ی عبور و نام کاربری وارد شده صحیح هستند یا خیر و آیا کاربر متناظر با آن هنوز فعال است. اگر بله، دو توکن دسترسی و به روز رسانی را تولید و به سمت کلاینت ارسال می‌کنیم.


پیاده سازی Refresh Token

پیاده سازی توکن به روز رسانی همانند عملیات لاگین است:
        [AllowAnonymous]
        [HttpPost("[action]")]
        public async Task<IActionResult> RefreshToken([FromBody]JToken jsonBody)
        {
            var refreshToken = jsonBody.Value<string>("refreshToken");
            if (string.IsNullOrWhiteSpace(refreshToken))
            {
                return BadRequest("refreshToken is not set.");
            }

            var token = await _tokenStoreService.FindTokenAsync(refreshToken);
            if (token == null)
            {
                return Unauthorized();
            }

            var (accessToken, newRefreshToken) = await _tokenStoreService.CreateJwtTokens(token.User).ConfigureAwait(false);
            return Ok(new { access_token = accessToken, refresh_token = newRefreshToken });
        }
با این تفاوت که در اینجا فقط یک Guid از سمت کاربر دریافت شده، سپس بر اساس این Guid، توکن و کاربر متناظر با آن یافت می‌شوند. سپس یک توکن جدید را بر اساس این اطلاعات تولید کرده و به سمت کاربر ارسال می‌کنیم.


پیاده سازی Logout

در سیستم‌های مبتنی بر JWT، پیاده سازی Logout سمت سرور بی‌مفهوم است؛ از این جهت که تا زمان انقضای یک توکن می‌توان از آن توکن جهت ورود به سیستم و دسترسی به منابع آن استفاده کرد. بنابراین تنها راه پیاده سازی Logout، ذخیره سازی توکن‌ها در بانک اطلاعاتی و سپس حذف آن‌ها در حین خروج از سیستم است. به این ترتیب اعتبارسنج سفارشی توکن‌ها، از استفاده‌ی مجدد از توکنی که هنوز هم معتبر است و منقضی نشده‌است، جلوگیری خواهد کرد:
        [AllowAnonymous]
        [HttpGet("[action]"), HttpPost("[action]")]
        public async Task<bool> Logout()
        {
            var claimsIdentity = this.User.Identity as ClaimsIdentity;
            var userIdValue = claimsIdentity.FindFirst(ClaimTypes.UserData)?.Value;

            // The Jwt implementation does not support "revoke OAuth token" (logout) by design.
            // Delete the user's tokens from the database (revoke its bearer token)
            if (!string.IsNullOrWhiteSpace(userIdValue) && int.TryParse(userIdValue, out int userId))
            {
                await _tokenStoreService.InvalidateUserTokensAsync(userId).ConfigureAwait(false);
            }
            await _tokenStoreService.DeleteExpiredTokensAsync().ConfigureAwait(false);
            await _uow.SaveChangesAsync().ConfigureAwait(false);

            return true;
        }


آزمایش نهایی برنامه

در فایل index.html، نمونه‌ای از متدهای لاگین، خروج و فراخوانی اکشن متدهای محافظت شده را مشاهده می‌کنید. این روش برای برنامه‌های تک صفحه‌ای وب یا SPA نیز می‌تواند مفید باشد و به همین نحو کار می‌کنند.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید.
  • #
    ‫۷ سال قبل، دوشنبه ۱۳ شهریور ۱۳۹۶، ساعت ۱۹:۰۷
    طبق این پست شما مبنی بر حذف ConfigureAwait ( false ) در برنامه‌های  ASP.NET Core  پس چرا شما از ConfigureAwait ( false ) استفاده میکنید؟
    • #
      ‫۶ سال و ۵ ماه قبل، دوشنبه ۳ اردیبهشت ۱۳۹۷، ساعت ۱۳:۰۹
      من الان دارم روی پروژه ای کار میکنم که هم نیاز به سایت دارد و هم نیاز به App اندروید. حالا میخواستم بدونم بهتره از کدام روش استفاده کنم برای اعتبار سنجی: 1-  روش اعتبار سنجی مبتنی بر JWT  که توی این پست شما توضیح دادین و یا  2- ASP.NET Core Identity   
      • #
        ‫۶ سال و ۵ ماه قبل، دوشنبه ۳ اردیبهشت ۱۳۹۷، ساعت ۱۳:۲۲
        از هر دو می‌توانید استفاده کنید. اولی مبتنی بر توکن‌ها است «معرفی JSON Web Token» و دومی به صورت پیش‌فرض مبتنی بر کوکی‌ها. توکن‌ها برای برنامه‌های SPA، مانند Angular بیشتر مرسوم هستند. امکان کار کردن با آ‌ن‌ها در برنامه‌های غیروبی هم هست. در کل هدف از بحث جاری، ارائه‌ی یک راه حل سبک، بجای ASP.NET Core Identity هست و تمام امکانات آن‌را شامل نمی‌شود.
        • #
          ‫۶ سال و ۵ ماه قبل، دوشنبه ۳ اردیبهشت ۱۳۹۷، ساعت ۱۳:۴۲
          چطور میتوانیم با استفاده از ASP.NET Core Identity در موبایل لاگین کنیم. یعنی بخوایم برنامه نویسی اندروید انجام بدیم و با استفاده از Api لاگین کنیم و یک سری کارهایی انجام بدیم
          • #
            ‫۶ سال و ۵ ماه قبل، دوشنبه ۳ اردیبهشت ۱۳۹۷، ساعت ۱۳:۵۸
            در اساس با پروژه‌ی غیروبی که عنوان شد تفاوتی نمی‌کند و بیشتر مرتبط است به مطلب «اعتبارسنجی مبتنی بر کوکی‌ها در ASP.NET Core 2.0 بدون استفاده از سیستم Identity» که با ASP.NET Core Identity مبتنی بر کوکی‌ها بیشتر شباهت دارد تا مطلب جاری که مبتنی بر توکن‌ها است. کوکی‌ها را در HttpClient فعال می‌کنید. زمانیکه درخواست لاگین را توسط آن ارسال کردید و عملیات لاگین در سمت سرور با موفقیت به پایان رسید، یکسری کوکی رمزنگاری شده، حاوی User Claims به سمت کلاینت ارسال می‌شوند. در دفعات بعدی، فقط این کوکی‌ها را به همراه HttpClient ارسال می‌کنید. به این صورت قسمت‌هایی که نیاز به اعتبارسنجی دارند کار می‌کنند.
            • #
              ‫۶ سال و ۱ ماه قبل، دوشنبه ۸ مرداد ۱۳۹۷، ساعت ۱۶:۲۳
              با سلام. من در پروژه DNTIdentity  برای تنظیمات لاگین کاربران غیر وبی  طبق پروژه  اعتبارسنجی مبتنی بر کوکی‌ها در ASP.NET Core 2.0 بدون استفاده از سیستم Identity تنظیمات  انجام دادم. و لاگین به درستی انجام میگیرد ولی مشکل اینجاس که اگه بخوام توی سایت به صورت پیش فرض لاگین کنم خطای زیر را می‌دهد. یعنی میخواد با خود اون لاگینی که برای کاربران غیر وبی ساختم وارد بشود. میخواستم بدونم آیا میشود تنظیم کرد که برای ورود کاربران  غیر وبی با api/account و برای خود سایت با account ای در خود پروژه پیاده سازی شده است وارد شوند
              {"error":{"code":"UnsupportedApiVersion","message":"The HTTP resource that matches the request URI 'http://localhost:45225/api/account/login?ReturnUrl=%2Fidentity%2Fhome' does not support HTTP method 'GET'."}}

              • #
                ‫۶ سال و ۱ ماه قبل، دوشنبه ۸ مرداد ۱۳۹۷، ساعت ۱۷:۴۲
                اگر از سیستم DNT Identity استفاده می‌کنید که خودش مبتنی بر کوکی‌ها است و نیازی ندارید هیچ پیاده سازی دیگری را به آن اضافه کنید. اگر مطلب «اعتبارسنجی مبتنی بر کوکی‌ها در ASP.NET Core 2.0 بدون استفاده از سیستم Identity» عنوان شد، فقط از این جهت بود که دیدی را جهت ارائه‌ی یک سیستم کوچک و خانگی شبیه به ASP.NET Core Identity ارائه دهد؛ نه اینکه هر دو را با هم استفاده کنید (جمع بستن دو سیستم اعتبارسنجی مبتنی بر کوکی‌ها ... غیرضروری است).
          • #
            ‫۶ سال و ۵ ماه قبل، دوشنبه ۳ اردیبهشت ۱۳۹۷، ساعت ۱۴:۵۰
            بعد از ارسال نام کاربری و پسوورد صحیح از طرف سرور یک توکن برای شما ارسال میشود . کافی است توکن در جایی مثلا Shared Preference یا دیتابیس لوکال ذخیره شود . از این به بعد برای فراخوانی تمامی منابع محافظت شده سرور شما باید توکن در http header قرار گیرد که اینکار معمولا با ثبت یک interceptor ( مثلا در Retrofit ) انجام میشود .
  • #
    ‫۶ سال و ۱۱ ماه قبل، یکشنبه ۱۶ مهر ۱۳۹۶، ساعت ۱۹:۴۴
    آیا راه حلی وجود دارد بدون ارسال headers در ajax  به سمت سرور ، اعتبار سنجی خود کار توسط خود اکشن سمت سرور انجام شود؟
    • #
      ‫۶ سال و ۱۱ ماه قبل، یکشنبه ۱۶ مهر ۱۳۹۶، ساعت ۲۲:۵۶
      در کل باید token ایی به سمت سرور ارسال شود (کاربر اطلاعات ویژه‌ای را به سمت سرور ارسال کند) تا مشخص شود که درخواست رسیده معتبر هست یا خیر؛ در غیراینصورت یک درخواست معمولی است که حاوی اطلاعات کاربر ارسال کننده‌ی آن نیست. البته الزامی به ارسال این توکن صرفا توسط هدر درخواست‌های Ajax ایی نیست. این مورد (نحوه‌ی تغییر محل ارسال توکن) در قسمت «.... OnMessageReceived برای حالتی است که توکن دریافتی، توسط هدر مخصوص Bearer به سمت سرور ارسال نمی‌شود...» در متن توضیح داده شده‌است. 
  • #
    ‫۶ سال و ۱۱ ماه قبل، چهارشنبه ۱۹ مهر ۱۳۹۶، ساعت ۱۲:۳۳
    قبل از publish  توکن کار می‌کنه و دسترسی به اکشن هام دارم. بعد از publish کردن پیغام Unauthorized 404 میده. آیا تنظیمات اضافه‌‌تری بر طبق توضیحات بالا برای زمان پابلیش کردن نیاز است؟
  • #
    ‫۶ سال و ۸ ماه قبل، یکشنبه ۱۷ دی ۱۳۹۶، ساعت ۱۷:۵۵
    تابع createAccessTokenAsync در کلاس TokenStoreService  زمانی که از Claim دارید یک شی ایجاد می‌کنید ، تاریخ را به ToUnixEpochDate تبدیل می‌کنید. علت این تبدیل چیست ؟
    new Claim(JwtRegisteredClaimNames.Iat, DateTime.UtcNow.ToUnixEpochDate().ToString(), ClaimValueTypes.Integer64),

    • #
      ‫۶ سال و ۸ ماه قبل، یکشنبه ۱۷ دی ۱۳۹۶، ساعت ۱۹:۱۰
      استاندارد آن به این صورت است:
       Its value MUST be a number containing a NumericDate value.
      و این NumericDate به نحو زیر باید تعریف شود (Section 2. Terminology):
       A JSON numeric value representing the number of seconds from 1970-01-01T00:00:00Z UTC until the specified UTC date/time, ignoring leap seconds. This is equivalent to the IEEE Std 1003.1, 2013 Edition [POSIX.1] definition "Seconds Since the Epoch", in which each day is accounted for by exactly 86400 seconds, other than that non-integer values can be represented. See RFC 3339 [RFC3339] for details regarding date/times in general and UTC in particular.
      نمونه‌ی استفاده‌ی از آن در متد getAccessTokenExpirationDateUtc ارائه شده‌است.  
      • #
        ‫۶ سال و ۸ ماه قبل، دوشنبه ۱۸ دی ۱۳۹۶، ساعت ۱۳:۰۰
        یک نکته‌ی تکمیلی
        متد ToUnixEpochDate با متد زیر:
        DateTimeOffset.UtcNow.ToUnixTimeSeconds() 
        هر دو یک مقدار را برمیگردونن.
  • #
    ‫۶ سال و ۵ ماه قبل، جمعه ۱۷ فروردین ۱۳۹۷، ساعت ۱۵:۵۲
    یک مثال تکمیلی: چگونه توکن‌ها و همچنین کوکی‌های Anti-forgery را توسط یک کلاینت یک غیر وبی به سمت سرور ارسال کنیم.
    • #
      ‫۶ سال و ۴ ماه قبل، شنبه ۸ اردیبهشت ۱۳۹۷، ساعت ۲۲:۴۲
      سلام وقت بخیر؛ در مثالی که زحمت کشیدین سیستم نقش‌ها به صورت استاتیک هست. چگونه میشه سیستمی رو طراحی کرد که نقش‌ها و دسترسی‌های کاربران به اکشن‌ها و کنترلهای مختلف رو به صورت پویا انجام داد. ممنون.
      • #
        ‫۶ سال و ۴ ماه قبل، شنبه ۸ اردیبهشت ۱۳۹۷، ساعت ۲۲:۵۴
        مبحث AddPolicy که در مطلب « سفارشی سازی ASP.NET Core Identity - قسمت پنجم - سیاست‌های دسترسی پویا » توضیح داده شده، وابستگی به ASP.NET Core Identity ندارد و مستقل از آن، مرتبط به جزء Security مجموعه ASP.NET Core است که قسمتی از آن در مطلب جاری استفاده شده‌است. بنابراین نکات آن‌را به همراه یک جدول اضافه‌تر RoleClaims، اینجا هم اضافه کنید، کار می‌کند و هیچ تفاوتی ندارد. 
        • #
          ‫۶ سال و ۲ ماه قبل، دوشنبه ۲۵ تیر ۱۳۹۷، ساعت ۱۷:۳۲
          سلام. وقت بخیر. من سیاست دسترسی پویا رو به jwt اضافه کردم. کلیه Claimها رو  به accesstoken اضافه میکنم و درست هم عملیات دسترسی چک میشود.
          {"jti":"26bdfd20-104f-45d4-a4e1-111044808041",
          "iss":"http://localhost:5000/",
          "iat":1531729854,
          "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier":"1",
          "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name":"Vahid",
          "DisplayName":"وحید",
          "http://schemas.microsoft.com/ws/2008/06/identity/claims/serialnumber":"046fb152a7474043952475cfa952cdc9",
          "http://schemas.microsoft.com/ws/2008/06/identity/claims/userdata":"1",
          "DynamicPermission":[":MyProtectedApi2:Get",
          ":MyProtectedEditorsApi:Get",
          ":MyProtectedApi3:Get",
          ":MyProtectedApi4:Get"],
          "http://schemas.microsoft.com/ws/2008/06/identity/claims/role":["Admin",
          "Editor",
          "User"],
          "nbf":1531729855,
          "exp":1531729975,
          "aud":"Any"}
           فقط این Claimها که در توکن اضافه میشود در سمت سرور از روی توکن ارسالی کاربر چک میشود؟
           public bool CanUserAccess(ClaimsPrincipal user, string area, string controller, string action)
                  {
                      var currentClaimValue = $"{area}:{controller}:{action}";
                      var securedControllerActions = _mvcActionsDiscoveryService.GetAllSecuredControllerActionsWithPolicy(ConstantPolicies.DynamicPermission);
                      if (!securedControllerActions.SelectMany(x => x.MvcActions).Any(x => x.ActionId == currentClaimValue))
                      {
                          throw new KeyNotFoundException($@"The `secured` area={area}/controller={controller}/action={action} with `ConstantPolicies.DynamicPermission` policy not found. Please check you have entered the area/controller/action names correctly and also it's decorated with the correct security policy.");
                      }
          
                      if (!user.Identity.IsAuthenticated)
                      {
                          return false;
                      }
          
                      if (user.IsInRole("Admin"))
                      {
                          // Admin users have access to all of the pages.
                          return true;
                      }
          
                      // Check for dynamic permissions
                      // A user gets its permissions claims from the `ApplicationClaimsPrincipalFactory` class automatically and it includes the role claims too.
          
          
                      //for check user has claim for access to action
                      return user.HasClaim(claim => claim.Type == ConstantPolicies.DynamicPermissionClaimType &&
                                                    claim.Value == currentClaimValue);
                  }
          امکان دستکاری توسط کاربر وجود ندارد؟ اگر اینطور است چطور این دسترسی‌ها رو در سمت سرور بعد از login در حافظه سرور ذخیره کنیم برای دفعات بعد؟
          • #
            ‫۶ سال و ۲ ماه قبل، دوشنبه ۲۵ تیر ۱۳۹۷، ساعت ۱۷:۴۵
            - قسمت «cfg.TokenValidationParameters» در سمت سرور، کار بررسی امضای دیجیتال و سایر مشخصات امنیتی را انجام می‌دهد. بنابراین امکان دستکاری آن در سمت کاربر نیست. اگر دستکاری شود، امضای دیجیتال آن در سمت سرور برگشت می‌خورد و در قسمت cfg.Events لاگ خواهد شد.
            - در حافظه‌ی سرور چیزی را نباید ذخیره کنید. سمت سرور برنامه‌های وب، stateless و یا بدون حالت است (یعنی قرار نیست در آن، پس از پایان درخواست، اطلاعاتی از کاربر در حافظه باقی بماند؛ بدلیل محدودیت منابع و مسایل امنیتی). اما این توکن‌ها در سمت کاربر عموما توسط روش local storage ذخیره می‌شوند و هربار از سرور واکشی نمی‌شوند. این توکن‌ها فقط هربار به ازای هر درخواست به منابع محافظت شده‌ی سمت سرور، «باید» از کلاینت به سمت سرور ارسال شوند. در غیراینصورت درخواست بدون توکن، یک درخواست معمولی اعتبارسنجی نشده‌است و ذخیره سازی اطلاعات در حافظه‌ی سرور عملا بی‌معنا است. اطلاعات بیشتر در مورد ذخیره سازی سمت کلاینت: «معرفی Local Storage و چند کتابخانه مرتبط» و «ذخیره سازی اطلاعات در مرورگر توسط برنامه‌های Angular»  
            • #
              ‫۶ سال و ۲ ماه قبل، دوشنبه ۲۵ تیر ۱۳۹۷، ساعت ۱۹:۰۱
              با تشکر . یعنی موقعی که توکن ایجاد میشود از claimها هم برای ایجاد آن استفاده میشود؟در اینجا
                private async Task<(string AccessToken, IEnumerable<Claim> Claims)> createAccessTokenAsync(User user)
                      {
                          var claims = new List<Claim>
                          {
                              // Unique Id for all Jwt tokes
                              new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString(), ClaimValueTypes.String, _configuration.Value.Issuer),
                              // Issuer
                              new Claim(JwtRegisteredClaimNames.Iss, _configuration.Value.Issuer, ClaimValueTypes.String, _configuration.Value.Issuer),
                              // Issued at
                              new Claim(JwtRegisteredClaimNames.Iat, DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64, _configuration.Value.Issuer),
                              new Claim(ClaimTypes.NameIdentifier, user.Id.ToString(), ClaimValueTypes.String, _configuration.Value.Issuer),
                              new Claim(ClaimTypes.Name, user.Username, ClaimValueTypes.String, _configuration.Value.Issuer),
                              new Claim("DisplayName", user.DisplayName, ClaimValueTypes.String, _configuration.Value.Issuer),
                              // to invalidate the cookie
                              new Claim(ClaimTypes.SerialNumber, user.SerialNumber, ClaimValueTypes.String, _configuration.Value.Issuer),
                              // custom data
                              new Claim(ClaimTypes.UserData, user.Id.ToString(), ClaimValueTypes.String, _configuration.Value.Issuer)
                          };
              
                         // add userclaims for permission
                          var clmsuser= await _rolesService.FindUserClaimesAsync(user.Id);
                          foreach (var cls in clmsuser)
                          {
                              claims.Add(new Claim(cls.ClaimType,cls.ClaimValue,ClaimValueTypes.String,_configuration.Value.Issuer));
                          }
                         
                          // add roles
                          var roles = await _rolesService.FindUserRolesAsync(user.Id);
                          foreach (var role in roles)
                          {
                              claims.Add(new Claim(ClaimTypes.Role, role.Name, ClaimValueTypes.String, _configuration.Value.Issuer));
                          }
              
                          var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration.Value.Key));
                          var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
                          var now = DateTime.UtcNow;
                          var token = new JwtSecurityToken(
                              issuer: _configuration.Value.Issuer,
                              audience: _configuration.Value.Audience,
                              claims: claims,
                              notBefore: now,
                              expires: now.AddMinutes(_configuration.Value.AccessTokenExpirationMinutes),
                              signingCredentials: creds);
                          return (new JwtSecurityTokenHandler().WriteToken(token), claims);
                      }

              و هر تغییر در claimها در سمت کاربر برابر میشود با  برگشت توکن دستکاری شده از سمت سرور و مسدود شدن دسترسی؟
              • #
                ‫۶ سال و ۲ ماه قبل، دوشنبه ۲۵ تیر ۱۳۹۷، ساعت ۱۹:۲۲
                بله. تمام اجزای توکن با هم یک موجودیت را تشکیل می‌دهند.
                • #
                  ‫۶ سال و ۲ ماه قبل، دوشنبه ۲۵ تیر ۱۳۹۷، ساعت ۱۹:۳۵
                  در سیستمهای بزرگ enterprise اگر از روش دسترسی پویا استفاده کنیم و همه claim‌ها را در توکن ذخیره کنیم، با هر درخواست کاربر همه claim‌ها با توکن کاربر به سمت سرور ارسال میشوند. این حجم اضافه شده مشکلی ایجاد نمی‌کند؟ یا بهتر است برای سیستمهای بزرگ برای هر بار درخواست کاربر، مجوز دسترسی از دیتابیس چک شود؟
                  • #
                    ‫۶ سال و ۲ ماه قبل، دوشنبه ۲۵ تیر ۱۳۹۷، ساعت ۱۹:۵۴
                    - در مقدمه مطلب «ذخیره سازی اطلاعات در مرورگر توسط برنامه‌های Angular» در مورد محدودیت حجم‌های حالت‌های مختلف ذخیره سازی اطلاعات در سمت کلاینت، بیشتر توضیح داده شده‌است.
                    - روش پیاده سازی dynamic permission شما و قرار دادن اطلاعات آن در توکن، در این حالت بی‌مورد است. از این جهت که به نظر قصد ندارید از اطلاعات آن در سمت کلاینت استفاده کنید (محدود کردن دسترسی به صفحات یک برنامه‌ی SPA و نه یک برنامه‌ی MVC). توکن و هرچیزی که در آن است جهت کاربردهای سمت کلاینت بیشتر باید مورد استفاده قرارگیرند تا سمت سرور. این بحث JWT برای برنامه‌های Angular و کلا SPA (تک صفحه‌ای وب) بیشتر استفاده می‌شود (سمت سرور Web API خالص، سمت کاربر SPA خالص). اگر برنامه‌ی شما چنین چیزی نیست، از آن استفاده نکنید.
                    چون اطلاعات دسترسی به صفحات به نظر سایت MVC شما مطلقا کاربردی در سمت کلاینت ندارند، آن‌را به توکن اضافه نکنید. در عوض در متد CanUserAccess، قسمت user.HasClaim را با کوئری گرفتن از بانک اطلاعاتی جایگزین کنید.
                    - مثال سمت کلاینت بحث جاری در سری «احراز هویت و اعتبارسنجی کاربران در برنامه‌های Angular» عمیق‌تر بررسی شده‌است و هدف از قسمت‌های مختلف توکن آن‌را جهت استفاده‌ی در سمت کلاینت (استفاده از نقش‌ها جهت دسترسی به صفحات برنامه‌ی سمت کلاینت Angular، استفاده از تاریخ انقضای توکن جهت بررسی اعتبار آن، استفاده از نام نمایشی قرار گرفته‌ی در توکن برای نمایش آن در سمت کلاینت و غیره)، بهتر درک خواهید کرد. در سمت سرور با داشتن Id شخص، مابقی را می‌توان از بانک اطلاعاتی کوئری گرفت و نیازی به سنگین کردن توکن نیست.
  • #
    ‫۶ سال و ۴ ماه قبل، دوشنبه ۱۰ اردیبهشت ۱۳۹۷، ساعت ۱۷:۳۰
    فرض کنید در سیستمی از refresh token استفاده نشده یعنی در واقع توکن در دیتابیس ذخیره نمی‌شود فقط در سمت سرور توکن به صورت in-memory تولید شده و کاربران تا زمان انقضای آن قادر به استفاده از API هستند این زمان نیز به 15 روز ست شده است. حالا بعد از اعمال روش ارائه شده در این مطلب (ذخیره‌سازی token و refresh token در دیتابیس) چطور می‌توانیم کاربرانی که از توکن قبلی استفاده می‌کنند را مجبور به Sign out کنیم؟ کلاینت‌ها هم اندروید و آی‌اوس هستند و چیزی حدود 100 کاربر در حال استفاده از اپلیکیشن است. روشی که به ذهنم رسید خالی کردن کش اپلیکیشن درون گوشی هست اما اینکار برای 100 یوزر منطقی نیست. آیا روشی وجود دارد که توکن‌ها اکتیو را از سمت سرور منقضی کنیم؟ 
    لاگ سمت سرور نیز این exception رو نمایش میدهد:
    Microsoft.IdentityModel.Tokens.SecurityTokenExpiredException: IDX10223: Lifetime validation failed. The token is expired. ValidTo: '[PII is hidden by default. Set the 'ShowPII' flag in IdentityModelEventSource.cs to true to reveal it.]', Current time: '[PII is hidden by default. Set the 'ShowPII' flag in IdentityModelEventSource.cs to true to reveal it.]'.
    at Microsoft.IdentityModel.Tokens.Validators.ValidateLifetime(Nullable`1 notBefore, Nullable`1 expires, SecurityToken securityToken, TokenValidationParameters validationParameters)
    at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateLifetime(Nullable`1 notBefore, Nullable`1 expires, JwtSecurityToken jwtToken, TokenValidationParameters validationParameters)
    at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateTokenPayload(JwtSecurityToken jwtToken, TokenValidationParameters validationParameters)
    at Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler.<HandleAuthenticateAsync>d__6.MoveNext()
    روش فوق به خوبی برای کاربران جدید کار می‌کنه اما مشکل اصلی کاربرانی هستند که در حال استفاده از توکن قبلی هستند.
    موردی که به ذهنم میرسه سفارشی‌سازی AuthorizeAttribute هستش (یعنی بررسی توکن اکتیو با نمونه‌ی موجود در دیتابیس اگر یکی نبود HandleUnauthorizedRequest رو به ریسپانس برمی‌گردونم) این مورد رو هم تست می‌کنم و نتیجه رو همینجا اعلام می‌کنم.
    • #
      ‫۶ سال و ۴ ماه قبل، دوشنبه ۱۰ اردیبهشت ۱۳۹۷، ساعت ۱۸:۰۲
      « ... حالا بعد از اعمال روش ارائه شده در این مطلب (ذخیره‌سازی token و refresh token در دیتابیس) چطور می‌توانیم کاربرانی که از توکن قبلی استفاده می‌کنند را مجبور به Sign out کنیم؟  ...»
      همان قسمت «تهیه یک اعتبارسنج توکن سفارشی» مطلب جاری هست که از نتیجه‌ی «پیاده سازی Logout» استفاده می‌کند. یا حتی می‌توانید در قسمت logout یک SerialNumber را هم تغییر دهید که به صورت یک Claim سفارشی در توکن قبلی وجود داشته باشد. عدم انطباق این مقادیر را در این اعتبارسنج سفارشی بررسی کنید.
      • #
        ‫۶ سال و ۴ ماه قبل، دوشنبه ۱۰ اردیبهشت ۱۳۹۷، ساعت ۲۲:۴۶
        آیا محدودیتی برای طول عمر توکن‌ها وجود نداره؟ مثلاً می‌تونیم این مقدار رو به بیشتر از دو سال تنظیم کنیم؟
        مشکل اصلی رفع شد ممنون؛ منتها این exception رو در سمت سرور وجود داره:
        Microsoft.IdentityModel.Tokens.SecurityTokenExpiredException: IDX10223: Lifetime validation failed. The token is expired. ValidTo: '[PII is hidden by default. Set the 'ShowPII' flag in IdentityModelEventSource.cs to true to reveal it.]', Current time: '[PII is hidden by default. Set the 'ShowPII' flag in IdentityModelEventSource.cs to true to reveal it.]'.
        at Microsoft.IdentityModel.Tokens.Validators.ValidateLifetime(Nullable`1 notBefore, Nullable`1 expires, SecurityToken securityToken, TokenValidationParameters validationParameters)
        at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateLifetime(Nullable`1 notBefore, Nullable`1 expires, JwtSecurityToken jwtToken, TokenValidationParameters validationParameters)
        at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateTokenPayload(JwtSecurityToken jwtToken, TokenValidationParameters validationParameters)
        at Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler.<HandleAuthenticateAsync>d__6.MoveNext()

        • #
          ‫۶ سال و ۴ ماه قبل، سه‌شنبه ۱۱ اردیبهشت ۱۳۹۷، ساعت ۰۰:۲۲
          - طول عمر کوکی یا توکن را هیچگاه به بیشتر از یک ماه تنظیم نکنید چون مشکل امنیتی ایجاد می‌کند.
          - این خطای «IDX10223: Lifetime validation failed. The token is expired» به این معنا است که توکن واقعا منقضی شده‌است یا به معنای تنظیم نبودن تاریخ و ساعت سمت سرور است. سعی کنید این مورد را با اینترنت هماهنگ کنید.
          - همچنین مقدار ClockSkew به معنای حد تحمل این عدم هماهنگی هست (بین ساعت و زمان صادر کننده‌ی تولید توکن و مصرف کننده‌ی آن). در اینجا به صفر تنظیم شده‌است. این‌را هم می‌توانید تا 5 دقیقه درنظر بگیرید.
          - خطای «PII is hidden by default» با تنظیم «IdentityModelEventSource.ShowPII = true» نمایش داده می‌شود.
  • #
    ‫۶ سال و ۲ ماه قبل، شنبه ۹ تیر ۱۳۹۷، ساعت ۱۴:۵۰
    سلام.  سناریویی رو در نظر بگیرید که کاربر(client) برای دسترسی به اکشن‌ها در کنترلر‌های api هر بار با درخواست خود access token که در مرحله قبلی ایجاد و ارسال شده را به سمت سرور ارسال کند. در این مورد میشه فقط با access token کار کرد و refresh token را از سیستم فوق حذف کرد. چون در مطلب فوق فرمودید : "refreshToken فقط یک Guid است. کار آن ساده سازی و به روز رسانی عملیات Login بدون ارائه‌ی نام کاربری و کلمه‌ی عبور است. ".  فلسفه استفاده از refreshToken در سیستم مذکور  با خود accessToken قابل پیاده سازی نیست؟ هر بار که کاربر درخواستی به سمت سرور ارسال کرد AccessToken را ارسال و پس از بررسی و احراز هویت کاربر دسترسی‌ها رو مشخص کند.
    • #
      ‫۶ سال و ۲ ماه قبل، شنبه ۹ تیر ۱۳۹۷، ساعت ۱۵:۰۳
      - مثال سمت کلاینت آن‌را بررسی کنید. ارسال اکسس‌توکن، جهت اعتبارسنجی در سمت سرور الزامی است و اختیاری نیست. توسط آن است که هویت کاربر مشخص می‌شود و گرنه درخواست رسیده عادی و اعتبارسنجی نشده‌است؛ همانند الزام به ارسال کوکی‌های سمت کلاینت ASP.NET Core Identity که اساس کار آن‌را تشکیل می‌دهد. بدون این کوکی، کاربر به هیچ قسمتی دسترسی نخواهد داشت. فقط چون در آنجا مرورگر کوکی‌ها را به صورت خودکار ارسال می‌کند، شاید متوجه حضور آن‌ها نشده‌اید و گرنه اساس کار یکی است. مفهوم refresh token در اینجا شبیه به پیاده سازی sliding expiration برای کوکی‌ها است. اطلاعات بیشتر: «معرفی JSON Web Token»
      - مثال سمت کلاینت بحث جاری در سری «احراز هویت و اعتبارسنجی کاربران در برنامه‌های Angular» عمیق‌تر بررسی شده‌است.
  • #
    ‫۶ سال و ۲ ماه قبل، شنبه ۱۶ تیر ۱۳۹۷، ساعت ۱۷:۱۹
    سلام؛ من میخواستم تو این حالت "تایید دو مرحله ای" رو در نظر بگیرم. سناریویی که به دهنم میرسه اینه که وقتی کاربر User و Password رو وارد میکنه و لاگین میزنه، من بیام اول صحتش رو بررسی کنم و بعد از تنظیماتش وضعیت "تایید دو مرحله ای" رو بخونم.
    اگر وضعیت تایید غیر فعال باشه توکن صادر کنم و اگر فعال باشه یه فلگ به همراه اینکه نوع تایید دو مرحله ایش رو چی انتخاب کرده برگردونم. چون User و Password رو سمت کلاینت دارم بعد از وارد کردن کد تایید دوباره موارد اطلاعات لاگین رو به همراه کد ارسال کنم به سرور. سمت سرور اول صحت کد رو بررسی کنم و بعدش مجددا اطلاعات لاگین رو، در صورت درست بودن توکن رو صادر کنم. به نظر شما ایده مناسب و امنی میتونه باشه؟ یا میشه بهتر و اصولی‌تر هم پیاده سازی کرد؟
  • #
    ‫۶ سال و ۱ ماه قبل، دوشنبه ۱ مرداد ۱۳۹۷، ساعت ۱۷:۱۸
    با سلام؛ من از پروژه DNTIdentity  استفاده کرده ام و برای اعتبار سنجی کاربران که با اپلیکیش وارد می‌شوند از jwt  میخواهم استفاده کنم و از پروژه ASPNETCore2JwtAuthentication.WebApp  کمک گرفتم و موارد را به پروژه تزریق کردم. و برای تست نیز از پروژه ASPNETCore2JwtAuthentication.ConsoleClient استفاده کردم ولی متاسفانه خطای زیر را می‌دهد. 
    Response status code does not indicate success: 500 (Internal Server Error)
    و سوال دیگر:در کلاس استارت اپ پروژه ASPNETCore2JwtAuthentication.WebApp  چرا  آدرس را با http://localhost:4200 مقدار دهی کردین و همچنین "Issuer": "http://localhost:5000/" در BearerTokens چرا از پورت ثابت استفاده شده است.
    • #
      ‫۶ سال و ۱ ماه قبل، دوشنبه ۱ مرداد ۱۳۹۷، ساعت ۱۸:۱۷
      - پروژه DNT Identity یک قسمت لاگر و گزارشگیری از اطلاعات آن دارد. مراجعه کنید به آن تا جزئیات خطای داخلی برنامه را مشاهده کنید. همچنین اگر برنامه‌ی وب را در حالت dotnet watch run هم اجرا کنید، این خطاها در کنسول لاگ می‌شوند.
      - پورت 4200 مرتبط است به «احراز هویت و اعتبارسنجی کاربران در برنامه‌های Angular» و پورت 5000 هم مرتبط است به پورت آزمایشی سرور اجرای کننده‌ی برنامه (dotnet watch run).
      • #
        ‫۶ سال و ۱ ماه قبل، دوشنبه ۱ مرداد ۱۳۹۷، ساعت ۱۸:۳۸
        ممنون؛ خروجی که توی برنامه نشون میده به این صورت است:
        Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request starting HTTP/1.1 POST http://localhost:45225/api/account/login application/json; charset=utf-8 
        Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request starting HTTP/1.1 POST http://localhost:45225/api/account/login application/json; charset=utf-8 
        Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker:Information: Route matched with {action = "Login", controller = "Account", area = ""}. Executing action DrugExchange.Controllers.AccountController.Login (DrugExchange)
        Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker:Information: Route matched with {action = "Login", controller = "Account", area = ""}. Executing action DrugExchange.Controllers.AccountController.Login (DrugExchange)
        Microsoft.EntityFrameworkCore.Infrastructure:Information: Entity Framework Core 2.1.1-rtm-30846 initialized 'ApplicationDbContext' using provider 'Microsoft.EntityFrameworkCore.SqlServer' with options: MaxPoolSize=128 CommandTimeout=180 
        Microsoft.EntityFrameworkCore.Infrastructure:Information: Entity Framework Core 2.1.1-rtm-30846 initialized 'ApplicationDbContext' using provider 'Microsoft.EntityFrameworkCore.SqlServer' with options: MaxPoolSize=128 CommandTimeout=180 
        Microsoft.AspNetCore.Mvc.Infrastructure.ObjectResultExecutor:Information: Executing ObjectResult, writing value of type 'DrugExchange.Common.ApiError'.
        Microsoft.AspNetCore.Mvc.Infrastructure.ObjectResultExecutor:Information: Executing ObjectResult, writing value of type 'DrugExchange.Common.ApiError'.
        Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker:Information: Executed action DrugExchange.Controllers.AccountController.Login (DrugExchange) in 42.4741ms
        Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker:Information: Executed action DrugExchange.Controllers.AccountController.Login (DrugExchange) in 42.4741ms
        Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request finished in 62.5345ms 500 application/ion+json; charset=utf-8
        Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request finished in 62.5345ms 500 application/ion+json; charset=utf-8
        Microsoft.AspNetCore.Server.Kestrel:Information: Connection id "0HLFGFSKPQEQC", Request id "0HLFGFSKPQEQC:00000001": the application completed without reading the entire request body.
        Microsoft.AspNetCore.Server.Kestrel:Information: Connection id "0HLFGFSKPQEQC", Request id "0HLFGFSKPQEQC:00000001": the application completed without reading the entire request body.
        البته اگر نیاز می‌بینید تا کد استارت اپ را هم بفرستم شاید بتونه کمک کنه.
  • #
    ‫۶ سال و ۱ ماه قبل، دوشنبه ۲۹ مرداد ۱۳۹۷، ساعت ۱۹:۴۶
    بعد از لاگین کردن و فراخوانی وب سرویس هایم پیغام 431 (Request Header Fields Too Large) در کنسول مرورگر نمایش میدهد
    باید اندازه‌ی request header‌ها رو بیشتر کرد ؟ چگونه می‌توان این کار را انجام داد؟
    • #
      ‫۶ سال و ۱ ماه قبل، دوشنبه ۲۹ مرداد ۱۳۹۷، ساعت ۲۰:۱۳
      این‌ها پیش‌فرض‌های سیستم هستند:

      http.sys default MaxRequestBytes 16384
      ngnix default large_client_header_buffers 4 * 8k = 32768
      apache.tomcat default maxHttpHeaderSize 8192 
      اگر اندازه‌ی هدر هر درخواست برنامه‌ی شما بیشتر از 16 کیلوبایت است، یعنی مشکل طراحی برنامه را دارید. کم و زیاد کردن این اندازه‌ها کمکی به شما نمی‌کنند.
  • #
    ‫۶ سال و ۱ ماه قبل، سه‌شنبه ۳۰ مرداد ۱۳۹۷، ساعت ۰۴:۴۳
    سلام؛ من اون قسمتی رو که در مورد OnMessageReceived  توضیح دادین رو متوجه نشدم. الان من لاگین کردم و توکن رو گرفتم طبق فرمایشات شما. اما نمی‌خوام از ajax استفاده کنم. پس نمی‌تونم طبق الگو به اون صورت که در مخزن اومده header رو پر کنم. الان چجوری میتونم در پیمایش بین اکشن‌ها و فرستادن request  بدون ajax، هدر و HttpContext.user رو پر کنم؟ اگر مثالی یا توضیح بیشتری دارین ممنون میشم ازتون
    • #
      ‫۶ سال و ۱ ماه قبل، سه‌شنبه ۳۰ مرداد ۱۳۹۷، ساعت ۰۵:۳۱
      - «... اما نمی‌خوام از ajax استفاده کنم. پس نمی‌تونم طبق الگو به اون صورت که در مخزن اومده header رو پر کنم ...»
      این مورد ربطی به Ajax بودن درخواست ندارد. اصل کار رعایت و ارسال پیامی توسط پروتکل HTTP است.
      - برای نمونه مراجعه کنید به مثال کلاینت غیر وب آن؛ یک مثال برنامه‌ی کنسول است که از HttpClient برای ساخت و ارسال پیام HTTP استفاده شده‌است. برای اجرای آن ابتدا مراجعه کنید به پوشه‌ی ASPNETCore2JwtAuthentication.WebApp و فایل _1-dotnet_run.bat آن‌را اجرا کنید تا سرور در آدرس http://localhost:5000 راه اندازی شود. سپس این برنامه‌ی کنسول را جداگانه اجرا کنید تا به سرور در حال اجرا متصل شود. 
  • #
    ‫۶ سال قبل، جمعه ۲ شهریور ۱۳۹۷، ساعت ۱۸:۳۴
    سلام؛ چطور می‌تونیم بعد از این که login با موفقیت انجام شد یک action رو صدا بزنیم که مثلا صفحه home رو نشون بده. در واقع بعد از اینکه token رو بعد از login بدست آوردم می‌خوام یه action رو با jwt toke صدا بزنم که بره اون صفحه رو load کنه. راهی مثل :window.location در javascript یا a href در html که بشه باهاش token رو فرستاد برای باز کردن action Authorize  شده. ممنون.
      • #
        ‫۶ سال قبل، جمعه ۲ شهریور ۱۳۹۷، ساعت ۱۹:۴۱
        ممنون از پاسخ اما من می‌خوام برگشت یک view باشه در واقع صفحه link بشه . در صورت امکان
         public IActionResult Get()
                {
                    //return Ok(new
                    //{
                    //    Id = 1,
                    //    Title = "Hello from My Protected Controller! [Authorize]",
                    //    Username = this.User.Identity.Name
                    //});
                    return View();
                }
        • #
          ‫۶ سال قبل، جمعه ۲ شهریور ۱۳۹۷، ساعت ۲۰:۲۷
          صفحات وب به همراه لینک‌ها و یا window.location و امثال آن امکان تنظیم header سفارشی درخواست‌های وب را ندارند؛ مگر اینکه یک درخواست async از نوع XMLHttpRequest به سمت سرور را سبب شوند. به همین جهت در حالت پیش‌فرض، تنظیم JWT Token به همراه آن‌ها میسر نیست. بنابراین در اینجا در صورت نیاز کار با Viewهای رندر شده‌ی در سمت سرور، از همان روش‌های Ajax که امکان تنظیم هدر را دارند، مانند نکات مطلب «بارگزاری PartialView با استفاده از jQuery در زمان اجرا» می‌توانید استفاده کنید. یا اینکه کلا برنامه‌ی وب خود را SPA تهیه کنید (مانند Angular) که مدیریت این قسمت از سرور جدا شده و به سمت کلاینت محول شود. در نظرات قبلی واژه‌ی SPA را در این صفحه جستجو کنید؛ چندین بار به آن ارجاع شده و توضیحات کافی داده شده‌است که هدف از مطلب جاری در عمل چیست.
  • #
    ‫۵ سال و ۱۱ ماه قبل، سه‌شنبه ۱۷ مهر ۱۳۹۷، ساعت ۰۱:۴۶
    سلام. اگر به جای اینکه AccessToken را به صورت دیتا برای کلاینت ارسال کنیم آنرا در Response Header قرار دهیم به طور مثال بعد از لاگین موفق به شکل ذیل:
    Response.Headers.Add("AccessToken", accessToken);
    در این حالت کلاینت هم این مقدار را ذخیره کرده و در درخواست‌های خود به سمت سرور ارسال می‌کند و در سمت سرور هم برای پیاده سازی امکان sliding expiration در رخداد OnTokenValidated بعد از چک کردن موارد مورد نظر دوباره AccessToken را ساخته و به 
    Response Header اضافه نماییم آیا همین قدر کافی نمی‌باشد به جای ذخیره فیزیکی Token در دیتابیس؟
  • #
    ‫۵ سال و ۱۰ ماه قبل، چهارشنبه ۲ آبان ۱۳۹۷، ساعت ۱۲:۳۱
    در متن مقاله فرمودین : 
    اگر متد AddAuthentication، مانند تنظیمات فوق به همراه این تنظیمات پیش‌فرض بود،
    خواستم ببینم اگر به طور مثال این تنظیمات :
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultSignInScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    لحاظ شوند و از سیستم Asp.net core Identity که در این مقاله سفارشی سازی ASP.NET Core Identity  توضیح دادین در کنار این روش استفاده گردد آیا تنظیمات بالا، برای بحث احراز هویت بصورت پیش فرض قرار میگیره و سیستم Identity مذکور دیگه از کوکی استفاده نمیکنه؟
    و آیا دیگه احتیاجی به ذکر این مورد در فیلتر authorize هست ؟ 
    [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]

    • #
      ‫۵ سال و ۱۰ ماه قبل، چهارشنبه ۲ آبان ۱۳۹۷، ساعت ۱۲:۴۵
      services.AddAuthentication(options =>
      {
          options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
          options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
      })
      .AddJwtBearer(options =>
      {
          options.....
      });
      پس از آن در یک برنامه‌ی مبتنی بر ASP.NET Core Identity، هر جائیکه فیلتر Authorize خالی وجود دارد، یعنی استفاده‌ی از حالت مبتنی بر کوکی‌ها. جهت استفاده‌ی از اطلاعات اعتبارسنجی مبتنی بر توکن‌ها، ذکر صریح AuthenticationSchemes ضروری است:
      روش الف) ذکر Schemes مد نظر در یک Policy جدید
      services.AddAuthorization(options =>
      {
          options.AddPolicy("Over18", policy =>
          {
              policy.AuthenticationSchemes.Add(JwtBearerDefaults.AuthenticationScheme);
              policy.RequireAuthenticatedUser();
              policy.Requirements.Add(new MinimumAgeRequirement());
          });
      });
      و سپس استفاده‌ی از آن
      [Authorize(Policy = "Over18")]
      روش ب) ذکر مستقیم Schemes در فیلتر Authorize
      [Authorize(Policy = ...., AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
      // Or
      [Authorize(Policy = ...., AuthenticationSchemes = CookieAuthenticationDefaults.AuthenticationScheme + ", " + JwtBearerDefaults.AuthenticationScheme)]
  • #
    ‫۵ سال و ۱۰ ماه قبل، چهارشنبه ۲ آبان ۱۳۹۷، ساعت ۱۷:۰۲
    با توجه به اینکه امکان حدس GUID‌های تولیدی بعدی ممکن می‌باشد؛ البته مشخصا این قضیه ضعف GUID نمی‌باشد و از روز اول هم با هدف Uniqueness طراحی شده است. حداقل دو راه حل زیر را می‌توان داشت:
    • استفاده از RandomNumberGenerator برای تولید رشته  تصادفی، که در این صورت حدس زدن رشته تولیدی بعدی سخت‌تر می‌باشد ولی با این حال برای بازه زمانی محدودی امکان تولید رشته یکتا را خواهد داشت.
    • در زمان درخواست توکن جدید، access_token منقضی شده را به همراه refresh_token به سرور ارسال کرده و اطلاعات کاربر را از آن توکن منقضی شده خارج کرده و از این طریق می‌توان بررسی کرد که refresh_token ارسالی برای کاربر درخواست کننده می‌باشد.
    • #
      ‫۵ سال و ۱۰ ماه قبل، پنجشنبه ۳ آبان ۱۳۹۷، ساعت ۱۴:۴۴
      در ویندوز، دات نت، از متدهای CoCreateGuid و UuidCreate ویندوز برای تولید Guid استفاده می‌کند. از زمان Windows 2000، اطلاعات بیت‌های اتفاقی این Guid از طریق Windows CryptGenRandom cryptographic API  تامین می‌شود که در نتیجه حداقل 122 bits آن اتفاقی است.
    • #
      ‫۵ سال و ۱۰ ماه قبل، پنجشنبه ۳ آبان ۱۳۹۷، ساعت ۱۵:۰۵
      نکته‌ی استفاده از RandomNumberGenerator اعمال شد.
  • #
    ‫۵ سال و ۹ ماه قبل، چهارشنبه ۷ آذر ۱۳۹۷، ساعت ۲۳:۰۴
    ما همه کار کردیم؛ token در بانک ذخیره کردیم. موقع بررسی توکن از بانک میگیریم سریال میگیریم همه چی رو بررسی میکنیم. خوب امنیت خیلی خوبی داره. ولی سوالم اینجاست که خارج از بحث استاندارد بودن، می‌تونیم یه Guid مثلا به کاربر بدیم حالا برای authorize هم با یه middleware سفارشی یا attribute‌های سفارشی بررسی بکنیم. مزیت این روش با  jwt چی میتونه باشه؟ هر چند قبل از دریافت توکن jwt به ما همه جور بررسی انجام میشه انقضا سریال امنیتی و الی اخر و به ما تحویل میده و ما هم یک سری موارد دیگه مازاد بر اون اجرا میکنیم که برای logout یک سری موارد دیگه باشه. ممنون.
    • #
      ‫۵ سال و ۹ ماه قبل، چهارشنبه ۷ آذر ۱۳۹۷، ساعت ۲۳:۲۰
      مطلب «معرفی JSON Web Token» را مطالعه کنید تا تفاوت‌های آن با یک Guid مشخص شود (از داشتن امضای دیجیتال جهت اطمینان حاصل کردن از عدم دستکاری آن توسط کاربر، داشتن تاریخ انقضاء تا امکان قرار دادن Claims و نقش‌های کاربر در آن جهت استفاده‌ی در برنامه‌های SPA و ...). به علاوه زمانیکه در اینجا یک زیرساخت استاندارد و آماده‌ی کار با آن و همچنین کاملا آزمایش شده و یک‌دست با تمام اجزای سیستم در ASP.NET Core وجود دارد، اختراع مجدد چرخ، خصوصا در مورد راه حل‌های امنیتی، توصیه نمی‌شود.
  • #
    ‫۵ سال و ۹ ماه قبل، پنجشنبه ۸ آذر ۱۳۹۷، ساعت ۱۷:۲۷
    جهت اطلاع
    امنیت refresh token تولیدی با تبدیل آن به یک JWT بجای صرفا یک Guid ساده، بهبود یافت؛ با این تغییرات. نکته‌های مهم آن دو متد جدید createRefreshToken و getRefreshTokenSerial هستند. در متد جدید getRefreshTokenSerial، کار اعتبارسنجی اطلاعات دریافتی از کاربر نیز صورت می‌گیرد؛ کاری که پیشتر در مورد یک Guid ساده میسر نبود.
    • #
      ‫۵ سال و ۹ ماه قبل، دوشنبه ۱۲ آذر ۱۳۹۷، ساعت ۱۲:۵۳
      وقتی که کاربر به سیستم لاگین میکند مقدار RefreshTokenIdHashSource  در دیتابیس برابر null خواهد بود.
                      RefreshTokenIdHashSource = string.IsNullOrWhiteSpace(refreshTokenSourceSerial) ?
                                                 null : _securityService.GetSha256Hash(refreshTokenSourceSerial),
      اگر کاربر اکشن Logout را فراخوانی کند در متد DeleteTokensWithSameRefreshTokenSourceAsync  با توجه به اینکه شرط حذف توکن  برابر بودن با مقدار  RefreshTokenIdHashSource در دیتابیس است و از آنجایی که مقدار این توکن هنگام ایجاد null بوده هیچ توکنی در اکشن Logout حذف نخواهد شد.
      مورد دوم اینکه علت ایجاد فیلد RefreshTokenIdHashSource به چه منظوری هست؟
      • #
        ‫۵ سال و ۹ ماه قبل، دوشنبه ۱۲ آذر ۱۳۹۷، ساعت ۱۴:۱۴
        « ... هیچ توکنی در اکشن Logout حذف نخواهد شد ... ».
        چندین Remove در TokenStoreService وجود دارند که یکی از آن‌ها مرتبط به RefreshTokenIdHashSource است. مقدار واقعی آن هم در زمان تولید یک Refresh Token جدید تنظیم می‌شود و نه در زمان لاگین. هدف این است که بتوان ردیابی کرد چه توکن جدیدی بر اساس یک Refresh Token قبلی، قابلیت صدور مجدد را پیدا کرده‌است. هدف پیدا کردن والد توکن(های) صادر شده‌ی جدید است. بنابراین اگر عملیات Refresh Token ای صورت نگرفته شده باشد، متد DeleteTokensWithSameRefreshTokenSourceAsync کار خاصی را انجام نخواهد داد.
  • #
    ‫۵ سال و ۶ ماه قبل، یکشنبه ۵ اسفند ۱۳۹۷، ساعت ۰۳:۵۵
    سلام؛ چند سوال. یک: برای بعضی از کاربران، با اینکه لاگین هستند ولی خود به خود به صفحه عدم دسترسی منتقل میشوند، در صورتی که در هدر همچنان دکمه‌های ارسال پست و ... که مخصوص کاربران لاگین شده هست در سایت قابل نمایش هست ، ولی اگر کاربر صفحه رو دستی رفرش کند (f5) کاربر دیگر لاگین نیست و مجدد نیاز به لاگین دارد.
    دو : با اینکه مقدار AccessTokenExpirationMinutes برابر 2 دقیقه قرار دادم ولی بعد از زمان تعین شده کاربر همچنان در سایت لاگین میباشد.
    • #
      ‫۵ سال و ۶ ماه قبل، یکشنبه ۵ اسفند ۱۳۹۷، ساعت ۰۵:۱۴
      دو نوع پیاده سازی سمت کلاینت برای این مطلب تهیه شده؛ مثال صفحه‌ی HTML آن در پوشه‌ی wwwroot و همچنین مثال کامل و جامع Angular آن. مثال صفحه‌ی HTML آن یک مثال ساده‌است برای توضیح مفاهیم و فاقد تایمری برای فعالسازی خودکار refresh token؛ درحالیکه مثال Angular آن دارای تایمر مخصوصی برای اینکار است و پیش از رسیدن به زمان انقضای access token، یک توکن جدید را درخواست می‌کند. همچنین در مثال HTML آن توکن‌ها در local storage مرورگر ذخیره نمی‌شوند و با refresh صفحه، متغیرهای موقتی جاوا اسکریپتی که توکن‌ها را ذخیره کرده بودند، پاک خواهند شد. اما در مثال Angular آن بحث ذخیره سازی در local storage پیاده سازی شده‌است و خیلی جزئیات دیگر.
  • #
    ‫۵ سال و ۶ ماه قبل، چهارشنبه ۸ اسفند ۱۳۹۷، ساعت ۰۳:۰۴
     با سلام
    ما در یکی از پروژه هایمان طبق موراد گفته شده کامل پیش رفته ایم و حال در نسخه production ابهاماتی داریم ، ممنون میشوم راهنمایی بفرمایید 
    1- یک کاربر در سیستم لاگین کرد و ما دستی در دیتابیس مقدار فیلد 
    AccessTokenExpiresDateTime

    را طوری تنظیم کردیم که منقضی شده باشد (مثلا ده ساعت به عقب بردیم ) ، در اولین رفرش توکن بعد از این ، انتظار داشتیم که کاربر از سایت خارج شود، ولی یک توکن جدید برای کاربر صادر شد و همچنان در سایت لاگین شده است .
    2--ما مقادیر را مانند زیر ست کرده ایم ، ولی رفرش توکن هر دو دقیقه یک بار اجرا میشود ، آیا این درست است یا باید یک دقیقه یک بار اجرا شود ؟
     "AccessTokenExpirationMinutes": 2,
     "RefreshTokenExpirationMinutes": 1,

    3- در این حالت که توکن‌ها در دیتابیس ذخیره می‌شوند، آیا اگر iis ریست شود  و یا application pool بعد از زمان 20 متوقف شود، تاثیر روی کاربران لاگین شده دارد ؟ 
  • #
    ‫۵ سال و ۲ ماه قبل، پنجشنبه ۶ تیر ۱۳۹۸، ساعت ۱۸:۰۱
    نکته تکمیلی
    افزودن اطلاعات اضافی به لیست Claimهای کاربر جاری پس از موفقیت آمیز بودن اعتبارسنجی توکن ارسالی:
    OnTokenValidated = async ctx =>
    {
        string oid = ctx.Principal.FindFirstValue("http://schemas.microsoft.com/identity/claims/objectidentifier");
    
        //Get EF context
        var db = ctx.HttpContext.RequestServices.GetRequiredService<AuthorizationDbContext>();
    
        //Check is user a super admin
        bool isSuperAdmin = await db.SuperAdmins.AnyAsync(a => a.ObjectId == oid);
        if (isSuperAdmin)
        {
            //Add claim if they are
            var claims = new List<Claim>
            {
                new Claim(ClaimTypes.Role, "superadmin")
            };
            var appIdentity = new ClaimsIdentity(claims);
    
            ctx.Principal.AddIdentity(appIdentity);
        }
    }
    All the existing claims will still be there after this. We are only adding new claims

  • #
    ‫۵ سال و ۱ ماه قبل، یکشنبه ۳۰ تیر ۱۳۹۸، ساعت ۲۱:۰۴
    معادل جدول UserToken (استفاده شده در این مقاله)  در identity چیست؟
  • #
    ‫۴ سال و ۹ ماه قبل، پنجشنبه ۱۴ آذر ۱۳۹۸، ساعت ۱۷:۲۲
    برای سایت‌های mvc چطوری باید توی هدر درخواستمون مقدار access token ارسال کنیم ؟ 
    زمانی که نخوایم دیگه از ajax استفاده کنیم ...
  • #
    ‫۴ سال و ۸ ماه قبل، سه‌شنبه ۳ دی ۱۳۹۸، ساعت ۱۳:۲۱
    با سلام؛ پروژه وب سایت همراه با اپ موبایل داریم. برای پیاده سازی احراز هویتی که هم در وب و هم در آپ موبایل  کارا باشه باید چه کنیم؟ آیا امکان این وجود داره همزمان هم از کوکی و توکن استفاده بشه؟
  • #
    ‫۴ سال و ۶ ماه قبل، شنبه ۳ اسفند ۱۳۹۸، ساعت ۱۷:۱۰
    جهت اطلاع
    این پروژه به 3.1.102 NET Core. ارتقاء داده شد.
  • #
    ‫۴ سال و ۵ ماه قبل، دوشنبه ۱۸ فروردین ۱۳۹۹، ساعت ۱۴:۳۱
    با سلام؛ دلیل استفاده از 
    services.AddAntiforgery (x => x.HeaderName = "X-XSRF-TOKEN");
    services.AddMvc (options => { options.Filters.Add (new 
                                      AutoValidateAntiforgeryTokenAttribute ()); });
    دو خط کد چیست؟
  • #
    ‫۴ سال و ۵ ماه قبل، جمعه ۲۲ فروردین ۱۳۹۹، ساعت ۰۹:۴۰
    با سلام، اگر بخواهم مدت زمان پیش فرض توکن کاربر را یک ماه در نظر بگیرم و دوم اینکه بر اساس آخرین فعالیت آن مدت زمان را به روز کنم چه کارهایی باید انجام بدم؟
    ممنون
  • #
    ‫۴ سال و ۴ ماه قبل، شنبه ۶ اردیبهشت ۱۳۹۹، ساعت ۰۳:۵۰
    آیا امکان انتقال تنظیمات jwt به لایه دیگر وجود دارد؟
    منظورم از تنظیمات:
                services.AddAuthorization(options =>
                        {
                            options.AddPolicy(CustomRoles.Admin, policy => policy.RequireRole(CustomRoles.Admin));
                            options.AddPolicy(CustomRoles.User, policy => policy.RequireRole(CustomRoles.User));
                            options.AddPolicy(CustomRoles.Editor, policy => policy.RequireRole(CustomRoles.Editor));
                        });
    
                // Needed for jwt auth.
                services
                    .AddAuthentication(options =>
                    {
                        options.DefaultChallengeScheme = siteSettings.JwtBearerDefaults.AuthenticationScheme;
                        options.DefaultSignInScheme = siteSettings.JwtBearerDefaults.AuthenticationScheme;
                        options.DefaultAuthenticateScheme = siteSettings.JwtBearerDefaults.AuthenticationScheme;
                    })
                    .AddJwtBearer(cfg =>
                    {
                        cfg.RequireHttpsMetadata = false;
                        cfg.SaveToken = true;
                        cfg.TokenValidationParameters = new TokenValidationParameters
                        {
                            ValidIssuer = Configuration["BearerTokens:Issuer"], // site that makes the token
                            ValidateIssuer = false, // TODO: change this to avoid forwarding attacks
                            ValidAudience = Configuration["BearerTokens:Audience"], // site that consumes the token
                            ValidateAudience = false, // TODO: change this to avoid forwarding attacks
                            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["BearerTokens:Key"])),
                            ValidateIssuerSigningKey = true, // verify signature to avoid tampering
                            ValidateLifetime = true, // validate the expiration
                            ClockSkew = TimeSpan.Zero // tolerance for the expiration date
                        };
    من نتونستم این سرویس هارو در لایه Ioc خودم ایجاد کنم. مشکلم هم بابت عدم وجود AddAuthentication   بود. هر چند من پکیج‌های زیر را اضافه کردم اما نتوانستم تمام سرویس‌ها رو تکمیل کنم
    Microsoft.AspNetCore.Authorization  و Microsoft.AspNetCore.Authorization.Policy من از نسخه 3.1.201 استفاده میکنم
    • #
      ‫۴ سال و ۴ ماه قبل، شنبه ۶ اردیبهشت ۱۳۹۹، ساعت ۰۴:۵۰
      مطابق جدول زیر، فقط اسمبلی مرتبط با متد الحاقی AddJwtBearer دارای بسته‌ی نیوگت 3x است و مابقی دیگر به صورت مجزایی تولید نمی‌شوند و جزئی از framework reference هستند .
       آخرین نگارش نیوگت موجود
       اسمبلی مرتبط
      متد الحاقی
      2.2.0  Microsoft.AspNetCore.Authorization.Policy, Version=3.1.0.0  ()AddAuthorization
      2.2.0  Microsoft.AspNetCore.Authentication, Version=3.1.0.0  ()AddAuthentication
      3.1.3  Microsoft.AspNetCore.Authentication.JwtBearer, Version=3.1.2.0  ()AddJwtBearer
       بنابراین نیاز به تنظیم target-framework و همچنین framework-reference هست:
      <Project Sdk="Microsoft.NET.Sdk">
        <PropertyGroup>
          <TargetFramework>netcoreapp3.1</TargetFramework>
        </PropertyGroup>
        <ItemGroup>
          <FrameworkReference Include="Microsoft.AspNetCore.App" />
        </ItemGroup>
      </Project>
    • #
      ‫۴ سال و ۴ ماه قبل، یکشنبه ۷ اردیبهشت ۱۳۹۹، ساعت ۱۶:۱۰
      با فرض اینکه <TargetFramework>netcoreapp3.1</TargetFramework>  به بالا تنظیم کردید کافیه پکیج زیر رو نصب کنین
      Install-Package Microsoft.AspNetCore.Authentication.JwtBearer

  • #
    ‫۴ سال و ۴ ماه قبل، شنبه ۶ اردیبهشت ۱۳۹۹، ساعت ۱۵:۰۰
    با سلام؛ اگه بخواهیم mvc core با این jwt و صفحات razor فعلا اجرا کنیم، مشکل اولین صفحه و Authorization مربوط به اون رو چطور انجام بدیم که کاربر هر درخواست صفحه ای داشت ابتدا به صفحه login هدایت بشه؟
    • #
      ‫۴ سال و ۴ ماه قبل، شنبه ۶ اردیبهشت ۱۳۹۹، ساعت ۱۵:۲۰
      - راه حل عمومی برای jQuery، React و Angular و غیره این است که جائیکه if (xhr.status === 401) را از سمت سرور دریافت کردید (401، خروجی خودکار فیلتر Authorize، در صورت شکست اعتبارسنجی است)، می‌توانید پیام خاصی و یا هدایت به صفحه‌ی خاصی را با فراخوانی دستور "window.location.href  = "/new/page/path انجام دهید.
      - چرا این راه حل عمومی است؟ چون قسمت
        $.ajax({
                      headers: { 'Authorization': 'Bearer ' + jwtToken },
      توسط مرورگرها توسط یک post-back ساده (مانند لاگین معمولی در صفحات مبتنی بر Razor)، به سمت سرور ارسال نمی‌شود. مرورگرها فقط کوکی‌ها را به صورت خودکار ارسال می‌کنند و نه توکن‌های سفارشی را. بنابراین اگر حتی از این روش JWT در برنامه‌های MVC هم استفاده کنید، مجبور به استفاده‌ی از Ajax خواهید بود تا هدرهای سفارشی را بتوانید به سمت سرور، جهت بررسی و اعتبارسنجی نهایی ارسال کنید. قسمتی از عملیات Ajax هم بررسی شکست عملیات و واکنش نشان دادن به status code دریافتی است.
  • #
    ‫۴ سال و ۴ ماه قبل، دوشنبه ۲۲ اردیبهشت ۱۳۹۹، ساعت ۰۳:۴۲
    من 2 تا پروژه وب API دارم که یکی برای ارتباط با دیتابیس داخلی هست و دیگری برای ارتباط با دیتابیس سرور هست. احراز هویت در API سمت سرور می‌باشد و در آنجا Token رو ایجاد میکنم آیا می‌شود از همین Token برای API دوم استفاده کرد و احراز هوییت رو بررسی کرد؟ فعلا امکان راه اندازی Identity server نمی‌باشد. ممنون میشم راهنمایی کنید تشکر
    • #
      ‫۴ سال و ۴ ماه قبل، دوشنبه ۲۲ اردیبهشت ۱۳۹۹، ساعت ۰۴:۰۷
      cfg.TokenValidationParameters را باید تغییر دهید تا مثلا ValidateIssuer را انجام ندهد و امثال آن. در کل باید پروسه‌ی Validation توکن آن‌را مستعد به حمله کنید. موفق باشید.
      • #
        ‫۴ سال و ۴ ماه قبل، دوشنبه ۲۲ اردیبهشت ۱۳۹۹، ساعت ۱۷:۱۹
        در هر دو سیستم از JWT با استفاده از روش شما پیاده سازی کردم فقط در پروژه Api دوم برای احراز هویت به APi اول مراجعه می‌کند و اگر Token معتبر بود وارد Action در APi اول می‌شود.
  • #
    ‫۴ سال و ۳ ماه قبل، شنبه ۲۷ اردیبهشت ۱۳۹۹، ساعت ۰۵:۱۶
    با سلام و خسته نباشید
    زمانیکه در کنترلر Account متد RefreshToken رو صدا میزنیم، به دلیل استفاده از متد :
    ...
    var token = await _tokenStoreService.FindTokenAsync(refreshTokenValue);
    ...
    و در ادامه استفاده این متد از متد :
    ...
    var refreshTokenSerial = _tokenFactoryService.GetRefreshTokenSerial(refreshTokenValue);
    ...
    در ادامه اعتبار سنجی انجام میشه:
    ...
    decodedRefreshTokenPrincipal = new JwtSecurityTokenHandler().ValidateToken(
                        refreshTokenValue,
                        new TokenValidationParameters
                        {
                            RequireExpirationTime = true,
                            ValidateIssuer = false,
                            ValidateAudience = false,
                            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration.Value.Key)),
                            ValidateIssuerSigningKey = true, // verify signature to avoid tampering
                            ValidateLifetime = true, // validate the expiration
                            ClockSkew = TimeSpan.Zero // tolerance for the expiration date
                        },
                        out _
                    );
    ...
    و در اینجا اعتبارسنجی شکست میخوره.
    برای حل این مشکل باید ValidateLifeTime=false بشه.
    تا بتونیم :
     بررسی RefreshToken و ایجاد توکن مجدد رو انجام بدیم
    در این حالت Logout کاربری که توکن آن منقضی شده چگونه خواهد بود؟
    همنین اگر توکن منقضی شود، AntiforgeryToken چه وضعیتی پیدا میکند؟ آیا برای متدهای RefreshToken و Logout درست کار میکند؟ برای درخواست‌های بعدی چطور؟ این کوکی چه مدت اعتبار دارد؟


    • #
      ‫۴ سال و ۳ ماه قبل، شنبه ۲۷ اردیبهشت ۱۳۹۹، ساعت ۰۶:۲۳
      این مسایل زوری نیست. توکنی را که منقضی شده به زور اعتبارسنجی نکن. منقضی شده؟ بفرستش بره صفحه‌ی لاگین دوباره از صفر شروع کنه. این مشکل ما نیست. مشکل هیچ کسی نیست. ریفرش توکن فقط یک «لطف» هست. الزام نیست. منقضی شده؟ بره لاگین کنه دوباره.
  • #
    ‫۳ سال و ۱۱ ماه قبل، چهارشنبه ۱۶ مهر ۱۳۹۹، ساعت ۰۱:۵۲
    با سلام؛ من از همین پروژه شما برای توسعه یک پروژه میخوام استفاده کنم. مشکلی عجیبی دارم. مدل ورودی به متدم دارای پارمترهای null می‌باشد و مقدار نمیگیره با postman  هم چک میکنم بازم هم به همین صورت: 
     مدل ارسالی با postman
  • #
    ‫۳ سال و ۹ ماه قبل، چهارشنبه ۲۱ آبان ۱۳۹۹، ساعت ۱۵:۳۶
    با سلام؛ بنده یک پروژه webapi ساختم که فرایند لاگین مشابه شما را انجام داده‌ام و یک پروژه سمت کلاینت دارم که asp.net core هست. الان برای ارسال درخواست، توکن رو به header میدم و درخواستم ارسال میکنم. اما پاسخ BadRequest هست

    اینجا قسمتی هست که درخواست سمت api ارسال میشه. ممنون میشم بهم بگین که کجا ی کار ایراد داره!

  • #
    ‫۳ سال و ۹ ماه قبل، یکشنبه ۲ آذر ۱۳۹۹، ساعت ۰۳:۳۱
    با سلام و تشکر؛
    در یک از متدها از سطر زیر استفاده شده:
    var claimsIdentity = this.User.Identity as ClaimsIdentity ;
    میخواستم بدونم آبجکت User که دراینجا اشاره شده، چطوری در دسترس قرار داره؟ و آیا همون کلاس User که در Domain  تعریف کردیم هست یا خیر؟
    اگر همان User که در Domain تعریف کردیم هست، با فرض اینکه من به اون کلاس User چند مشخصه جدید (مثلا MyCustomId) اضافه کرده باشم، به اون فیلد MyCustomId مثل این سطر که اشاره کردم دسترسی خواهم داشت ؟ یا باید بعد از به دست آوردن شناسه کاربر (userId) آبجکت کاربر رو از پایگاه داده فراخوانی کنم و بعد اون فیلد MyCustomId   رو ازش بردارم.
    • #
      ‫۳ سال و ۹ ماه قبل، یکشنبه ۲ آذر ۱۳۹۹، ساعت ۰۴:۵۱
      خیر. شیء this.User با اطلاعات جدول کاربران، تناظر یک به یک ندارد. از نگارش‌های پیشین ASP.NET ، هنوز هم اطلاعات شیء User مانند User.Identity.Name در ASP.NET Core نیز در دسترس هستند. به این ترتیب زمانیکه کاربری به سیستم وارد شد، دیگر نیازی نیست تا جهت یافتن Name او، از بانک اطلاعاتی کوئری گرفت. خاصیت Name یاد شده به صورت خودکار از کوکی رمزنگاری شده‌ی و یا در اینجا از توکن او دریافت شده و در اختیار برنامه قرار می‌گیرد. این Name در ASP.NET Core Identity، اصطلاحا یک User Claim پیش‌فرض نام دارد و به صورت خودکار ایجاد و مقدار دهی می‌شود. روش مقدار دهی اولیه‌ی آن هم در متد createAccessTokenAsync مشخص است. هر زمانیکه این توکن به سمت سرور ارسال می‌شود، پس از اعتبارسنجی توکن و پذیرش آن، این Claims هم پردازش شده و جزئی از اطلاعات شیء this.User می‌شوند.
  • #
    ‫۳ سال و ۶ ماه قبل، جمعه ۲۴ بهمن ۱۳۹۹، ساعت ۰۲:۵۹
    با سلام و تشکر از شما. نحوه استفاده رفرش توکن در یک برنامه ویندوزی wpf به چه صورت هست؟ پس از رفرش توکن  و اجرای متد‌ها در ویومدل پیام 401 صادر میکند. آیا  روش به روز رسانی توکن در برنامه ویندوزی با یک برنامه وب متفاوت است؟
  • #
    ‫۳ سال و ۱ ماه قبل، پنجشنبه ۲۴ تیر ۱۴۰۰، ساعت ۱۴:۴۰
    اینجور که من متوجه شدم با استفاده از این روش نمی‌توان کاری کرد که با بستن مرورگر، کاربر از حساب کاربری خود خارج شود!
    درسته؟
  • #
    ‫۳ سال و ۱ ماه قبل، پنجشنبه ۲۴ تیر ۱۴۰۰، ساعت ۱۴:۵۱
    چطور می‌توان توکن را هنگام ارجاع از یک سایت ثانوی با متد (GET) دریافت کرد مثلا از سایت درگاه بانکی یا...؟
    آیا تنها راه استفاده از کوکی هست؟ یا راه حلی از طریق هدر هم وجود دارد؟
  • #
    ‫۲ سال و ۸ ماه قبل، یکشنبه ۲۱ آذر ۱۴۰۰، ساعت ۱۵:۰۰
    یک نکته: روش تعیین اعتبار دستی یک JWT
    اگر خواستید رشته‌ی JWT دریافتی را در سمت سرور و بر اساس تنظیمات ابتدایی برنامه، بدون نیاز به تکرار آن‌ها و به صورت دستی اعتبارسنجی کنید، روش انجام کار به صورت زیر است:
    public class TokenFactoryService
    {
        private readonly JwtBearerOptions _jwtBearerOptions;
    
        public TokenFactoryService(IOptionsSnapshot<JwtBearerOptions> jwtBearerOptions)
        {
            if (jwtBearerOptions == null)
            {
                throw new ArgumentNullException(nameof(jwtBearerOptions));
            }
    
            _jwtBearerOptions = jwtBearerOptions.Value ?? throw new ArgumentNullException(nameof(jwtBearerOptions));
        }
    
        // From: https://github.com/dotnet/aspnetcore/blob/a450cb69b5e4549f5515cdb057a68771f56cefd7/src/Security/Authentication/JwtBearer/src/JwtBearerHandler.cs
        public bool ValidateJwt(string token)
        {
            foreach (var validator in _jwtBearerOptions.SecurityTokenValidators)
            {
                try
                {
                    if (validator.CanReadToken(token))
                    {
                        validator.ValidateToken(token, _jwtBearerOptions.TokenValidationParameters, out _);
                    }
                }
                catch
                {
                    return false;
                }
            }
    
            return true;
        }
    }
  • #
    ‫۲ سال و ۷ ماه قبل، شنبه ۹ بهمن ۱۴۰۰، ساعت ۱۷:۴۲
    سلام.
    پروژه اعتبارسنجی jwt را مطابق همین پست انجام دادم و روی لوکال روی پورت 4983 اجرا شده است.  پروژه فرانت انگولار روی لوکال پورت 4200 در حال اجراست. و برای لاگین مستقیما به 4983 ریکوئست میفرستد و توکن دریافت میکند. سورس اصلی بکند روی پروژه دیگری روی لوکال پورت 7744 در حال اجراست. فرانت بعد از لاگین دیگر ارتباطی با 4983 ندارد و از این به بعد همه ریکوئست‌ها را به پورت 7744 ارسال میکند. در واقع 4983 فقط پروژه jwt به تنهایی است. فرانت وقتی ریکوئستی را به 7744 میفرستد نیاز به ولید شدن توکن دارد و باید توکن را برای بررسی به 4983 ارسال کند. و در startup 7744 این کد نوشته شده است:
                services.AddAuthentication(options =>
                {
                    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
                })
                .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
                {
                    options.Authority = "http://localhost:4983/";
                    options.Audience = "Any";
                    options.RequireHttpsMetadata = false;
                    options.Events = new JwtBearerEvents();
                    options.Configuration = new OpenIdConnectConfiguration();
                    options.Events.OnTokenValidated = async context =>
                    {
                        IEnumerable<Claim> userClaims = await ReadClaimsFromCacheAsync(context);
    
                        if (ThereIsNoCache(userClaims))
                        {
                            userClaims = await GetUserInfoAsync(context, authority);
                            await StoreInCacheAsync(userClaims, context);
                        }
    
                        context.Principal = new ClaimsPrincipal(new ClaimsIdentity(userClaims, "jwt", JwtClaimTypes.Subject, "roles"));
                    };
                    options.Events.OnAuthenticationFailed = async context =>
                    {
                        await TestInvalid(context);
                    };
                });
    که خطای زیر را دریافت میکنم:
    IDX10500: Signature validation failed. No security keys were provided to validate the signature.
    ممنون میشم راهنمایی بفرمایید. (نکته: یک کنترلر تستی روی 4983 نوشته ام که بعد از لاگین مستقیم از فرانت کال کردم و بدون مشکل توکن ولید شد و به کنترلر و اکشن موردنظر دسترسی پیدا کرد. ولی وقتی اعتبارسنجی از طریق 7744 صورت میگیرد خطا میدهد.)
    • #
      ‫۲ سال و ۷ ماه قبل، شنبه ۹ بهمن ۱۴۰۰، ساعت ۱۷:۵۳
      خواص IOptionsSnapshot<JwtBearerOptions> jwtBearerOptions تزریقی نال هستند. به صورت مستقیمی این مقادیر را از تنظیمات برنامه تامین کنید. در نهایت کدهای قبلی به صورت زیر تغییر می‌کنند:
      public class TokenValidatorService : ITokenValidatorService
      {
          private readonly BearerTokensOptionsDto _configuration;
      
          public TokenValidatorService(IOptionsSnapshot<SiteSettingsDto> configuration)
          {
              if (configuration == null)
              {
                  throw new ArgumentNullException(nameof(configuration));
              }
      
              _configuration = configuration.Value?.BearerTokens ?? throw new ArgumentNullException(nameof(configuration));
          }
      
          public async Task<bool> IsValidJwtAsync(string token)
          {
              var tokenHandler = new JwtSecurityTokenHandler();
              try
              {
                  var claimsPrincipal = tokenHandler.ValidateToken(token, new TokenValidationParameters
                  {
                      ValidIssuer = _configuration.Issuer, // site that makes the token
                      ValidateIssuer = true,
                      ValidAudience = _configuration.Audience, // site that consumes the token
                      ValidateAudience = true,
                      IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration.Key)),
                      ValidateIssuerSigningKey = true, // verify signature to avoid tampering
                      ValidateLifetime = true, // validate the expiration
                      ClockSkew = TimeSpan.Zero // tolerance for the expiration date
                  }, out var securityToken);
      
                  //var (success, _, _) = await IsValidClaimsPrincipalAsync(claimsPrincipal, securityToken);
                  //return success;
                  return true;
              }
              catch
              {
                  return false;
              }
          }
      }
  • #
    ‫۲ سال و ۶ ماه قبل، پنجشنبه ۱۲ اسفند ۱۴۰۰، ساعت ۱۴:۴۳
    جهت اطلاع
    این پروژه به ASP.NET Core 6x ارتقاء داده شد.
  • #
    ‫۱ سال و ۱۰ ماه قبل، دوشنبه ۲ آبان ۱۴۰۱، ساعت ۰۳:۳۲
    با سلام امکان Convert آبجکت context از نوع TokenValidatedContext به byte[] یا json وجود نداره؟
    دلیل پرسش بنده به این علت هست که اگر بر اساس طراحی معماری میکروسرویس مبحث validate توکن رو بخوایم به سرویس دیگری محول کنیم اونوقت مجبوریم تو Extension متد مثلا از طریق Grpc این context رو بفرستیم به متد validate داخل سرویس مربوط به validate
    ممکنه بفرمائید راه حل شما چیست؟
    متشکرم
    • #
      ‫۱ سال و ۱۰ ماه قبل، دوشنبه ۲ آبان ۱۴۰۱، ساعت ۱۰:۳۷
      از کل TokenValidatedContext به ClaimsPrincipal آن نیاز است. فقط همین قسمت را Serialize کنید. منظور این است که داده‌هایی را که واقعا نیاز است Serialize کنید؛ نه کل شیء را. همچنین این داده‌ها را هم می‌توان به اشیاء ساده‌تری نگاشت کرد.
      • #
        ‫۱ سال و ۱۰ ماه قبل، دوشنبه ۲ آبان ۱۴۰۱، ساعت ۱۷:۳۲
        متشکرم . این آبجکت رو بنظر به راحتی نمیشه serialize کرد .  چطور میشه ClaimsPrincipal  رو به درستی serialize و deserialize کرد؟
        • #
          ‫۱ سال و ۱۰ ماه قبل، دوشنبه ۲ آبان ۱۴۰۱، ساعت ۱۷:۵۰
          نیازی به اینکار نیست. token خام رسیده، یک string است. این داده‌ی خام را به متد Task IsValidJwtAsync(string token) ارسال کنید و از آن claimsPrincipal و securityToken را دریافت کنید.
  • #
    ‫۱ سال و ۹ ماه قبل، دوشنبه ۷ آذر ۱۴۰۱، ساعت ۱۵:۵۶
    از NET 7.0. به بعد میتوانید از دستور dotnet user-jwts create برای تولید توکن JWT تستی حین development استفاده کنید (+)
    dotnet user-jwts create --scope "greetings_api" --role "admin"


  • #
    ‫۱ سال و ۴ ماه قبل، سه‌شنبه ۱۵ فروردین ۱۴۰۲، ساعت ۲۲:۱۵
    با سلام؛ زمانیکه ساعت سیستم کاربر با ساعت سیستم سرور مغایرت داشته باشد، مثلا 1 ساعت جلوتر باشد، پس از لاگین، کاربر AccessToken  نال  و خطای 401  بر میگرداند. ممکن است کاربری از این موضوع مطلع نباشد تا ساعت Device خود را تغییر دهد. راه حل این مشکل چیست؟ آیا با ارسال ساعت فعلی کاربر به سرور جهت استفاده در تولید توکن مشکل حل میشود؟ 
  • #
    ‫۱ سال قبل، پنجشنبه ۲ شهریور ۱۴۰۲، ساعت ۱۵:۵۷
    با سلام؛ توکن از یک سرور احراز هویت دریافت میشه و در سمت کلاینت  (sessionStorage )  ذخیره می‌شود با درخواست‌های ajax مشکلی نیست  به هدر اضافه می‌شود ( headers: { Authorization: "Bearer " + sessionStorage.getItem("access_token") }, )
    ولی برای خواست‌های غیر از ajax چطور توکن از sessionStorage به هدر اضافه کنیم.
    • #
      ‫۱ سال قبل، پنجشنبه ۲ شهریور ۱۴۰۲، ساعت ۱۷:۰۹
      پوشه پروژه‌ای که در انتهای مطلب ذکر شده، مثال‌های متنوعی (An ajax sample, A console App, An angular App, Postman Tests, Integration Tests) دارد.
  • #
    ‫۹ ماه قبل، یکشنبه ۵ آذر ۱۴۰۲، ساعت ۱۷:۳۵
    ارتقاء به دات نت 8
    اگر این پروژه رو به دات نت 8 ارتقاء بدیم در کلاس TokenValidatorService دیگر نمیتوان با استفاده از accessToken.RawData توکن را خواند که آن را در پایگاه داده مورد بررسی قرار بدیم.
    if (!(context.SecurityToken is JwtSecurityToken accessToken) ||
        string.IsNullOrWhiteSpace(accessToken.RawData) ||
        !await _tokenStoreService.IsValidTokenAsync(accessToken.RawData, userId))
    {
        context.Fail("This token is not in our database.");
        return;
    }
    علت اینکار این است که در دات نت 8 کلاس JsonWebToken جایگزین JwtSecurityToken شده است ^ و همچنین برای گرفتن توکن باید از متد UnsafeToString استفاده کرد.
    if (!(context.SecurityToken is JsonWebToken accessToken) ||
        string.IsNullOrWhiteSpace(accessToken.UnsafeToString()) ||
        !await tokenStoreService.IsValidTokenAsync(accessToken.UnsafeToString(), userId))
    {
        context.Fail("This token is not in our database.");
        return;
    }
    علت این جایگزینی بهبود عملکرد تا 30 درصد و پردازش Async در کلاس JsonWebToken میباشد.